Skip to content

Commit 3a1da25

Browse files
authored
VS-164: LinqVerbosity config option and IQueryable inference (#90)
1 parent d7bb0c8 commit 3a1da25

File tree

14 files changed

+300
-52
lines changed

14 files changed

+300
-52
lines changed

src/MongoDB.Analyzer/Core/Linq/LinqExpressionProcessor.cs

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,18 @@ internal static class LinqExpressionProcessor
2020
{
2121
public static ExpressionsAnalysis ProcessSemanticModel(MongoAnalysisContext context, AnalysisType analysisType = AnalysisType.Linq)
2222
{
23-
if (analysisType != AnalysisType.Linq && analysisType != AnalysisType.EF)
23+
var isLinq = analysisType == AnalysisType.Linq;
24+
var isEF = analysisType == AnalysisType.EF;
25+
if (!isEF && !isLinq)
2426
{
2527
throw new ArgumentOutOfRangeException(nameof(analysisType), analysisType, "Unsupported analysis type");
2628
}
2729

30+
if (isLinq && !ShouldAnalyzeLinqVerbosity(context))
31+
{
32+
return new();
33+
}
34+
2835
var semanticModel = context.SemanticModelAnalysisContext.SemanticModel;
2936
var syntaxTree = semanticModel.SyntaxTree;
3037
var root = syntaxTree.GetRoot();
@@ -77,24 +84,30 @@ public static ExpressionsAnalysis ProcessSemanticModel(MongoAnalysisContext cont
7784

7885
processedSyntaxNodes.Add(node);
7986

80-
// Validate IMongoQueryable node candidate
8187
if (deepestIQueryableNode == null)
8288
{
89+
// No IQueryable node found
90+
continue;
91+
}
92+
93+
var deepestInvocationNode = deepestIQueryableNode.NestedInvocations().LastOrDefault();
94+
if (deepestInvocationNode != null && semanticModel.GetTypeInfo(deepestInvocationNode).Type.IsSystemCollectionOrArray())
95+
{
96+
// Skip system collections and arrays
8397
continue;
8498
}
8599

86100
var mongoQueryableTypeInfo = semanticModel.GetTypeInfo(deepestIQueryableNode);
87101
var isDBSet = mongoQueryableTypeInfo.Type.IsDBSet();
88102
var isIQueryable = mongoQueryableTypeInfo.Type.IsIQueryable();
89103

90-
if (!isDBSet && analysisType == AnalysisType.EF ||
91-
((!isIQueryable || isDBSet) && analysisType == AnalysisType.Linq))
104+
if (!isDBSet && isEF || ((!isIQueryable || isDBSet) && isLinq))
92105
{
93106
// Allow only DBSet<T> in EF analysis
94107
// Allow only IQueryable<T> except DBSet<T> in LINQ analysis
95108
continue;
96109
}
97-
110+
98111
if (mongoQueryableTypeInfo.Type is not INamedTypeSymbol mongoQueryableNamedType ||
99112
mongoQueryableNamedType.TypeArguments.Length != 1 ||
100113
!mongoQueryableNamedType.TypeArguments[0].IsSupportedMongoCollectionType())
@@ -104,8 +117,8 @@ public static ExpressionsAnalysis ProcessSemanticModel(MongoAnalysisContext cont
104117

105118
try
106119
{
107-
if ((analysisType == AnalysisType.Linq && PreanalyzeLinqExpression(node, semanticModel, invalidExpressionNodes)) ||
108-
(analysisType == AnalysisType.EF && PreanalyzeEFExpression(node, semanticModel, invalidExpressionNodes, mongoQueryableNamedType)))
120+
if ((isLinq && PreanalyzeLinqExpression(node, semanticModel, invalidExpressionNodes)) ||
121+
(isEF && PreanalyzeEFExpression(node, semanticModel, invalidExpressionNodes, mongoQueryableNamedType)))
109122
{
110123
var generatedMongoQueryableTypeName = typesProcessor.ProcessTypeSymbol(mongoQueryableNamedType.TypeArguments[0]);
111124

@@ -133,8 +146,8 @@ public static ExpressionsAnalysis ProcessSemanticModel(MongoAnalysisContext cont
133146

134147
var linqAnalysis = new ExpressionsAnalysis()
135148
{
136-
AnalysisNodeContexts = analysisContexts.ToArray(),
137-
InvalidExpressionNodes = invalidExpressionNodes.ToArray(),
149+
AnalysisNodeContexts = [.. analysisContexts],
150+
InvalidExpressionNodes = [.. invalidExpressionNodes],
138151
TypesDeclarations = typesProcessor.TypesDeclarations
139152
};
140153

@@ -143,6 +156,20 @@ public static ExpressionsAnalysis ProcessSemanticModel(MongoAnalysisContext cont
143156
return linqAnalysis;
144157
}
145158

159+
private static bool ShouldAnalyzeLinqVerbosity(MongoAnalysisContext context) =>
160+
context.Settings.LinqAnalysisVerbosity switch
161+
{
162+
LinqAnalysisVerbosity.None => false,
163+
LinqAnalysisVerbosity.Medium => HasMongoDBNamespace(context.SemanticModelAnalysisContext.SemanticModel.SyntaxTree),
164+
LinqAnalysisVerbosity.All => true,
165+
_ => throw new ArgumentOutOfRangeException(nameof(context.Settings.LinqAnalysisVerbosity))
166+
};
167+
168+
private static bool HasMongoDBNamespace(SyntaxTree syntaxTree) =>
169+
syntaxTree.GetRoot().DescendantNodes().Any(s =>
170+
s is UsingDirectiveSyntax usingDirectiveSyntax &&
171+
usingDirectiveSyntax.Name.ToFullString().StartsWith("MongoDB.Driver"));
172+
146173
private static bool PreanalyzeLinqExpression(SyntaxNode linqExpressionNode, SemanticModel semanticModel, List<InvalidExpressionAnalysisNode> invalidLinqExpressionNodes)
147174
{
148175
var result = true;

src/MongoDB.Analyzer/Core/Poco/PocoExpressionProcessor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public static ExpressionsAnalysis ProcessSemanticModel(MongoAnalysisContext cont
2121
if (context.Settings.PocoAnalysisVerbosity == PocoAnalysisVerbosity.None ||
2222
context.Settings.PocoLimit <= 0)
2323
{
24-
return default;
24+
return new();
2525
}
2626

2727
var semanticModel = context.SemanticModelAnalysisContext.SemanticModel;

src/MongoDB.Analyzer/Core/Settings/MongoDBAnalyzerSettings.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,21 @@ internal enum PocoAnalysisVerbosity
2626
All
2727
}
2828

29+
[JsonConverter(typeof(StringEnumConverter))]
30+
internal enum LinqAnalysisVerbosity
31+
{
32+
None,
33+
Medium,
34+
All
35+
}
36+
2937
internal record MongoDBAnalyzerSettings(
3038
[DefaultValue(false)] bool OutputDriverVersion = false,
3139
[DefaultValue(false)] bool OutputInternalExceptions = false,
3240
[DefaultValue(false)] bool OutputInternalLogsToFile = false,
3341
[DefaultValue(null)] string LogFileName = null,
3442
[DefaultValue(true)] bool SendTelemetry = true,
43+
[DefaultValue(LinqAnalysisVerbosity.Medium)] LinqAnalysisVerbosity LinqAnalysisVerbosity = LinqAnalysisVerbosity.Medium,
3544
[DefaultValue(PocoAnalysisVerbosity.Medium)] PocoAnalysisVerbosity PocoAnalysisVerbosity = PocoAnalysisVerbosity.Medium,
3645
[DefaultValue(500)] int PocoLimit = 500,
3746
[DefaultValue(true)] bool EnableVariableTracking = true)

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

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ internal static class SymbolExtensions
2323
private const string NamespaceMongoDBDriver = "MongoDB.Driver";
2424
private const string NamespaceMongoDBLinq = "MongoDB.Driver.Linq";
2525
private const string NamespaceSystem = "System";
26+
private const string NamespaceSystemCollectionsGeneric = "System.Collections.Generic";
2627
private const string NamespaceSystemLinq = "System.Linq";
2728

28-
private static readonly HashSet<string> s_supportedBsonAttributes = new()
29-
{
29+
private static readonly HashSet<string> s_supportedBsonAttributes =
30+
[
3031
"BsonConstructorAttribute",
3132
"BsonDateTimeOptionsAttribute",
3233
"BsonDefaultValueAttribute",
@@ -43,32 +44,32 @@ internal static class SymbolExtensions
4344
"BsonNoIdAttribute",
4445
"BsonRequiredAttribute",
4546
"BsonTimeSpanOptionsAttribute"
46-
};
47+
];
4748

48-
private static readonly HashSet<string> s_supportedBsonTypes = new()
49-
{
49+
private static readonly HashSet<string> s_supportedBsonTypes =
50+
[
5051
"MongoDB.Bson.BsonDocument",
5152
"MongoDB.Bson.BsonObjectId",
5253
"MongoDB.Bson.BsonType",
5354
"MongoDB.Bson.BsonValue",
5455
"MongoDB.Bson.Serialization.Options.TimeSpanUnits"
55-
};
56+
];
5657

57-
private static readonly HashSet<string> s_supportedCollections = new()
58-
{
58+
private static readonly HashSet<string> s_supportedCollections =
59+
[
5960
"System.Collections.Generic.IEnumerable<T>",
6061
"System.Collections.Generic.IList<T>",
6162
"System.Collections.Generic.List<T>"
62-
};
63+
];
6364

64-
private static readonly HashSet<string> s_supportedSystemTypes = new()
65-
{
65+
private static readonly HashSet<string> s_supportedSystemTypes =
66+
[
6667
"System.DateTime",
6768
"System.DateTimeKind",
6869
"System.DateTimeOffset",
6970
"System.TimeSpan",
7071
"System.Type"
71-
};
72+
];
7273

7374
public static (bool IsNullable, ITypeSymbol underlyingType) DiscardNullable(this ITypeSymbol typeSymbol) =>
7475
typeSymbol?.OriginalDefinition.SpecialType switch
@@ -253,24 +254,39 @@ public static bool IsSupportedSystemType(this ITypeSymbol typeSymbol, string ful
253254
(typeSymbol.SpecialType != SpecialType.None || s_supportedSystemTypes.Contains(fullTypeName)) &&
254255
typeSymbol?.ContainingNamespace?.ToDisplayString() == NamespaceSystem;
255256

257+
public static bool IsSystemCollectionOrArray(this ITypeSymbol typeSymbol)
258+
{
259+
if (typeSymbol is IArrayTypeSymbol)
260+
return true;
261+
262+
if (typeSymbol is not INamedTypeSymbol namedTypeSymbol)
263+
return false;
264+
265+
266+
if (namedTypeSymbol.ContainingNamespace?.ToDisplayString() == NamespaceSystemCollectionsGeneric)
267+
return true;
268+
269+
return false;
270+
}
271+
256272
private static SyntaxToken[] GetPublicFieldModifiers() =>
257-
new[] { SyntaxFactory.Token(SyntaxKind.PublicKeyword) };
273+
[SyntaxFactory.Token(SyntaxKind.PublicKeyword)];
258274

259275
private static AccessorDeclarationSyntax[] GetReadOnlyPropertyAccessors() =>
260-
new[] { SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
261-
.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)) };
276+
[ SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
277+
.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)) ];
262278

263279
private static SyntaxToken[] GetReadOnlyPublicFieldModifiers() =>
264-
new[] { SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword) };
280+
[SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword)];
265281

266282
private static AccessorDeclarationSyntax[] GetReadWritePropertyAccessors() =>
267-
new[] { SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
283+
[ SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
268284
.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)), SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
269-
.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)) };
285+
.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)) ];
270286

271287
private static AccessorDeclarationSyntax[] GetWriteOnlyPropertyAccessors() =>
272-
new[] { SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
273-
.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)) };
288+
[ SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
289+
.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)) ];
274290

275291
private static bool ImplementsOrIsInterface(this ITypeSymbol typeSymbol, string @namespace, string interfaceName)
276292
{

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,9 +176,6 @@ public static IEnumerable<ExpressionSyntax> NestedInvocations(this SyntaxNode sy
176176

177177
expressionSyntax = GetNextNestedInvocation(expressionSyntax);
178178
}
179-
180-
ExpressionSyntax GetNextNestedInvocation(SyntaxNode syntaxNode) =>
181-
((syntaxNode as InvocationExpressionSyntax)?.Expression as MemberAccessExpressionSyntax)?.Expression;
182179
}
183180

184181
public static SyntaxNode TrimParenthesis(this SyntaxNode syntaxNode)
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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 System.Collections.Generic;
16+
using System.Linq;
17+
using MongoDB.Analyzer.Tests.Common.DataModel;
18+
// Do not include MongoDB.Driver namespace here, as it is part of the test case.
19+
20+
namespace MongoDB.Analyzer.Tests.Common.TestCases.Linq
21+
{
22+
public sealed class LinqVerbosity : TestCasesBase
23+
{
24+
[MQL("Aggregate([{ \"$match\" : { \"IsRetired\" : true } }])", linqAnalysisVerbosity: LinqAnalysisVerbosity.All)]
25+
[MQL("Aggregate([{ \"$match\" : { \"IsRetired\" : true } }])", linqAnalysisVerbosity: LinqAnalysisVerbosity.All)]
26+
public void VerbosityAll_diagnostics_is_generated_for_IQueryable()
27+
{
28+
_ = GetMongoQueryable<Person>().Where(person => person.IsRetired == true);
29+
30+
_ = from person in GetMongoQueryable<Person>()
31+
where person.IsRetired == true
32+
select person;
33+
}
34+
35+
[NoDiagnostics(linqAnalysisVerbosity: LinqAnalysisVerbosity.All)]
36+
public void VerbosityAll_no_diagnostics_for_system_collections()
37+
{
38+
var personsArray = new Person[] { null, null };
39+
_ = personsArray.AsQueryable().Where(person => person.IsRetired == true);
40+
_ = personsArray.AsQueryable().AsQueryable().Where(person => person.IsRetired == true);
41+
_ = personsArray.AsEnumerable().AsQueryable().Where(person => person.IsRetired == true);
42+
43+
var personsList = new List<Person> { null, null };
44+
_ = personsList.AsQueryable().Where(person => person.IsRetired == true);
45+
46+
var personsHashset = new HashSet<Person> { null, null };
47+
_ = personsHashset.AsQueryable().Where(person => person.IsRetired == true);
48+
}
49+
50+
[NoDiagnostics(linqAnalysisVerbosity: LinqAnalysisVerbosity.Medium)]
51+
public void VerbosityMedium_no_diagnostics_for_IQueryable()
52+
{
53+
_ = GetMongoQueryable<Person>().Where(person => person.IsRetired == true);
54+
}
55+
56+
[NoDiagnostics(linqAnalysisVerbosity: LinqAnalysisVerbosity.Medium)]
57+
public void VerbosityMedium_no_diagnostics_for_system_collections()
58+
{
59+
var personsArray = new Person[] { null, null };
60+
_ = personsArray.AsQueryable().Where(person => person.IsRetired == true);
61+
_ = personsArray.AsQueryable().AsQueryable().Where(person => person.IsRetired == true);
62+
_ = personsArray.AsEnumerable().AsQueryable().Where(person => person.IsRetired == true);
63+
64+
var personsList = new List<Person> { null, null };
65+
_ = personsList.AsQueryable().Where(person => person.IsRetired == true);
66+
67+
var personsHashset = new HashSet<Person> { null, null };
68+
_ = personsHashset.AsQueryable().Where(person => person.IsRetired == true);
69+
}
70+
71+
[NoDiagnostics(linqAnalysisVerbosity: LinqAnalysisVerbosity.None)]
72+
public void VerbosityNone_no_diagnostics()
73+
{
74+
_ = GetMongoQueryable<Person>().Where(person => person.IsRetired == true);
75+
76+
_ = from person in GetMongoQueryable<Person>()
77+
where person.IsRetired == true
78+
select person;
79+
}
80+
81+
[NoDiagnostics(linqAnalysisVerbosity: LinqAnalysisVerbosity.Undefined)]
82+
public void VerbosityUndefined_defaults_to_medium_no_diagnostics_for_IQueryable_without_MBD_namespace()
83+
{
84+
_ = GetMongoQueryable<Person>().Where(person => person.IsRetired == true);
85+
}
86+
}
87+
}

0 commit comments

Comments
 (0)