diff --git a/samples/BasicSample/BasicSample.csproj b/samples/BasicSample/BasicSample.csproj
index bce1f728..d6901035 100644
--- a/samples/BasicSample/BasicSample.csproj
+++ b/samples/BasicSample/BasicSample.csproj
@@ -10,7 +10,7 @@
-
+
diff --git a/samples/BasicSample/BuildersSample.cs b/samples/BasicSample/BuildersSample.cs
index 4f7362b9..6f8b195d 100644
--- a/samples/BasicSample/BuildersSample.cs
+++ b/samples/BasicSample/BuildersSample.cs
@@ -20,6 +20,19 @@ namespace BasicSample
{
public class BuildersSample
{
+ public void AtlasSearch()
+ {
+ var mongoClient = new MongoClient(@"mongodb://localhost:27017");
+ var db = mongoClient.GetDatabase("testdb");
+ var moviesCollection = db.GetCollection("movies");
+
+ // Search definition (analyzer provides mql as information message)
+ var searchTitle = Builders.Search.Wildcard(p => p.Title, "Green D*");
+
+ // MQL is displayed for 'searchTitle' variables
+ moviesCollection.Aggregate().Search(searchTitle);
+ }
+
public async Task> GetMovies(double minScore, Genre genre, string titleSearchTerm)
{
var mongoClient = new MongoClient(@"mongodb://localhost:27017");
diff --git a/src/MongoDB.Analyzer.Helpers/Builders/Renderer.cs b/src/MongoDB.Analyzer.Helpers/Builders/Renderer.cs
index baa8fa85..0b65e7ea 100644
--- a/src/MongoDB.Analyzer.Helpers/Builders/Renderer.cs
+++ b/src/MongoDB.Analyzer.Helpers/Builders/Renderer.cs
@@ -17,7 +17,7 @@
namespace MongoDB.Analyzer.Helpers.Builders
{
- public static class Renderer
+ public static partial class Renderer
{
public static string Render(FilterDefinition filterDefinition)
{
diff --git a/src/MongoDB.Analyzer.Helpers/Builders/Renderer_2_19_and_higher.cs b/src/MongoDB.Analyzer.Helpers/Builders/Renderer_2_19_and_higher.cs
new file mode 100644
index 00000000..0827b516
--- /dev/null
+++ b/src/MongoDB.Analyzer.Helpers/Builders/Renderer_2_19_and_higher.cs
@@ -0,0 +1,28 @@
+// Copyright 2021-present MongoDB Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using MongoDB.Bson.Serialization;
+using MongoDB.Driver.Search;
+
+namespace MongoDB.Analyzer.Helpers.Builders
+{
+ public static partial class Renderer
+ {
+ public static string Render(SearchDefinition searchDefinition)
+ {
+ var renderedBuildersDefinition = searchDefinition.Render(BsonSerializer.LookupSerializer(), BsonSerializer.SerializerRegistry);
+ return renderedBuildersDefinition.ToString();
+ }
+ }
+}
diff --git a/src/MongoDB.Analyzer.Helpers/MongoDB.Analyzer.Helpers.csproj b/src/MongoDB.Analyzer.Helpers/MongoDB.Analyzer.Helpers.csproj
index 78e8bb25..8b20a7f7 100644
--- a/src/MongoDB.Analyzer.Helpers/MongoDB.Analyzer.Helpers.csproj
+++ b/src/MongoDB.Analyzer.Helpers/MongoDB.Analyzer.Helpers.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/src/MongoDB.Analyzer/Core/Builders/AnalysisCodeGenerator.cs b/src/MongoDB.Analyzer/Core/Builders/AnalysisCodeGenerator.cs
index 4f2cf5e4..400aa80d 100644
--- a/src/MongoDB.Analyzer/Core/Builders/AnalysisCodeGenerator.cs
+++ b/src/MongoDB.Analyzer/Core/Builders/AnalysisCodeGenerator.cs
@@ -20,12 +20,14 @@ namespace MongoDB.Analyzer.Core.Builders;
internal static class AnalysisCodeGenerator
{
private static readonly SyntaxTree[] s_helpersSyntaxTrees;
+ private static readonly SyntaxTree s_renderer_2_19_and_higher;
private static readonly BuildersMqlGeneratorTemplateBuilder.SyntaxElements s_mqlGeneratorSyntaxElements;
private static readonly ParseOptions s_parseOptions;
static AnalysisCodeGenerator()
{
s_helpersSyntaxTrees = GetCommonCodeResources(ResourceNames.Builders.Renderer);
+ s_renderer_2_19_and_higher = GetCodeResource(ResourceNames.Builders.Renderer_2_19_and_higher);
var mqlGeneratorSyntaxTree = GetCodeResource(ResourceNames.Builders.MqlGenerator);
s_mqlGeneratorSyntaxElements = BuildersMqlGeneratorTemplateBuilder.CreateSyntaxElements(mqlGeneratorSyntaxTree);
@@ -50,6 +52,11 @@ public static CompilationResult Compile(MongoAnalyzerContext context, Expression
mqlGeneratorSyntaxTree
};
+ if (referencesContainer.Version >= BuildersAnalysisConstants.Version_2_19_and_higher)
+ {
+ syntaxTrees.Add(s_renderer_2_19_and_higher);
+ }
+
var compilation = CSharpCompilation.Create(
BuildersAnalysisConstants.AnalysisAssemblyName,
syntaxTrees,
diff --git a/src/MongoDB.Analyzer/Core/Builders/BuildersAnalysisConstants.cs b/src/MongoDB.Analyzer/Core/Builders/BuildersAnalysisConstants.cs
index a808c4f7..7400ff3f 100644
--- a/src/MongoDB.Analyzer/Core/Builders/BuildersAnalysisConstants.cs
+++ b/src/MongoDB.Analyzer/Core/Builders/BuildersAnalysisConstants.cs
@@ -17,4 +17,6 @@ namespace MongoDB.Analyzer.Core.Builders;
internal static class BuildersAnalysisConstants
{
public const string AnalysisAssemblyName = "DynamicProxyGenAssembly2";
+
+ public static readonly Version Version_2_19_and_higher = Version.Parse("2.19.0");
}
diff --git a/src/MongoDB.Analyzer/Core/HelperResources/ResourceNames.cs b/src/MongoDB.Analyzer/Core/HelperResources/ResourceNames.cs
index 1e3876a2..3e407a20 100644
--- a/src/MongoDB.Analyzer/Core/HelperResources/ResourceNames.cs
+++ b/src/MongoDB.Analyzer/Core/HelperResources/ResourceNames.cs
@@ -30,6 +30,7 @@ internal static class Builders
{
public const string MqlGenerator = $"Builders.{nameof(MqlGenerator)}";
public const string Renderer = $"Builders.{nameof(Renderer)}";
+ public const string Renderer_2_19_and_higher = $"Builders.{nameof(Renderer_2_19_and_higher)}";
}
public const string EmptyCursor = nameof(EmptyCursor);
diff --git a/src/MongoDB.Analyzer/Core/Utilities/SymbolExtensions.cs b/src/MongoDB.Analyzer/Core/Utilities/SymbolExtensions.cs
index 67f8750b..bfc5335c 100644
--- a/src/MongoDB.Analyzer/Core/Utilities/SymbolExtensions.cs
+++ b/src/MongoDB.Analyzer/Core/Utilities/SymbolExtensions.cs
@@ -63,11 +63,12 @@ public static bool IsBuilder(this ITypeSymbol typeSymbol) =>
"FilterDefinitionBuilder" or
"IndexKeysDefinitionBuilder" or
"IndexKeysDefinitionExtensions" or
- "SortDefinitionBuilder" or
- "SortDefinitionExtensions" or
"ProjectionDefinitionBuilder" or
"ProjectionDefinitionExtensions" or
"PipelineDefinitionBuilder" or
+ "SearchDefinitionBuilder" or
+ "SortDefinitionBuilder" or
+ "SortDefinitionExtensions" or
"UpdateDefinitionBuilder" => true,
_ => false
};
@@ -82,9 +83,10 @@ public static bool IsBuilderDefinition(this ITypeSymbol typeSymbol) =>
{
"FilterDefinition" or
"IndexKeysDefinition" or
- "SortDefinition" or
"ProjectionDefinition" or
"PipelineDefinition" or
+ "SearchDefinition" or
+ "SortDefinition" or
"UpdateDefinition" => true,
_ => false
};
diff --git a/tests/MongoDB.Analyzer.Tests.Common.ClassLibrary/MongoDB.Analyzer.Tests.Common.ClassLibrary.csproj b/tests/MongoDB.Analyzer.Tests.Common.ClassLibrary/MongoDB.Analyzer.Tests.Common.ClassLibrary.csproj
index e116f315..0c38ff7c 100644
--- a/tests/MongoDB.Analyzer.Tests.Common.ClassLibrary/MongoDB.Analyzer.Tests.Common.ClassLibrary.csproj
+++ b/tests/MongoDB.Analyzer.Tests.Common.ClassLibrary/MongoDB.Analyzer.Tests.Common.ClassLibrary.csproj
@@ -8,7 +8,7 @@
-
+
runtime
diff --git a/tests/MongoDB.Analyzer.Tests.Common.TestCases/Builders/BuildersAtlasSearch.cs b/tests/MongoDB.Analyzer.Tests.Common.TestCases/Builders/BuildersAtlasSearch.cs
new file mode 100644
index 00000000..94e03a19
--- /dev/null
+++ b/tests/MongoDB.Analyzer.Tests.Common.TestCases/Builders/BuildersAtlasSearch.cs
@@ -0,0 +1,150 @@
+// Copyright 2021-present MongoDB Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Collections.Generic;
+using MongoDB.Analyzer.Tests.Common.DataModel;
+using MongoDB.Bson;
+using MongoDB.Driver;
+using MongoDB.Driver.GeoJsonObjectModel;
+using MongoDB.Driver.Search;
+
+namespace MongoDB.Analyzer.Tests.Common.TestCases.Builders
+{
+ public sealed class BuildersAtlasSearch : TestCasesBase
+ {
+ private static readonly GeoJsonPolygon s_testPolygon =
+ new GeoJsonPolygon(
+ new GeoJsonPolygonCoordinates(
+ new GeoJsonLinearRingCoordinates(
+ new List()
+ {
+ new GeoJson2DGeographicCoordinates(-161.323242, 22.512557),
+ new GeoJson2DGeographicCoordinates(-152.446289, 22.065278),
+ new GeoJson2DGeographicCoordinates(-156.09375, 17.811456),
+ new GeoJson2DGeographicCoordinates(-161.323242, 22.512557)
+ })));
+
+ [BuildersMQL("{ \"autocomplete\" : { \"query\" : \"My address\", \"path\" : \"Address\" } }", DriverVersions.V2_19_AndHigher, 1, 2, 3)]
+ public void Autocomplete()
+ {
+ _ = Builders.Search.Autocomplete(m => m.Address, "My address");
+ _ = Builders.Search.Autocomplete("Address", "My address");
+ _ = Builders.Search.Autocomplete("Address", "My address");
+ }
+
+ [BuildersMQL("{ \"equals\" : { \"value\" : true, \"path\" : \"IsRetired\" } }", DriverVersions.V2_19_AndHigher, 1, 2, 3)]
+ public void Equals()
+ {
+ _ = Builders.Search.Equals(m => m.IsRetired, true);
+ _ = Builders.Search.Equals("IsRetired", true);
+ _ = Builders.Search.Equals("IsRetired", true);
+ }
+
+ [BuildersMQL("{ \"near\" : { \"origin\" : 1, \"pivot\" : 2, \"path\" : \"SiblingsCount\" } }", DriverVersions.V2_19_AndHigher)]
+ [BuildersMQL("{ \"near\" : { \"origin\" : NumberLong(5), \"pivot\" : NumberLong(2), \"path\" : \"SiblingsCount\" } }", DriverVersions.V2_19_AndHigher, 2, 3)]
+ public void Near()
+ {
+ Builders.Search.Near(p => p.SiblingsCount, 1, 2);
+ Builders.Search.Near("SiblingsCount", 5L, 2L);
+ Builders.Search.Near("SiblingsCount", 5L, 2L);
+ }
+
+ [BuildersMQL("{ \"phrase\" : { \"query\" : \"Columbia\", \"path\" : \"Address.Province\" } }", DriverVersions.V2_19_AndHigher, 1, 2, 3)]
+ public void Phrase()
+ {
+ _ = Builders.Search.Phrase(m => m.Address.Province, "Columbia");
+ _ = Builders.Search.Phrase("Address.Province", "Columbia");
+ _ = Builders.Search.Phrase("Address.Province", "Columbia");
+ }
+
+ [BuildersMQL("{ \"queryString\" : { \"defaultPath\" : \"Name\", \"query\" : \"constant string\" } }", DriverVersions.V2_19_AndHigher, 1, 2, 3)]
+ public void QueryString()
+ {
+ _ = Builders.Search.QueryString(m => m.Name, "constant string");
+ _ = Builders.Search.QueryString("Name", "constant string");
+ _ = Builders.Search.QueryString("Name", "constant string");
+ }
+
+ [BuildersMQL("{ \"wildcard\" : { \"query\" : [\"foo\", \"bar\"], \"path\" : \"Name\" } }", DriverVersions.V2_19_AndHigher)]
+ [BuildersMQL("{ \"wildcard\" : { \"query\" : \"A\", \"path\" : \"Name\" } }", DriverVersions.V2_19_AndHigher, 2, 3)]
+ public void Wildcard()
+ {
+ _ = Builders.Search.Wildcard(m => m.Name, new[] { "foo", "bar" });
+ _ = Builders.Search.Wildcard("Name", "A");
+ _ = Builders.Search.Wildcard("Name", "A");
+ }
+
+ [BuildersMQL("{ \"regex\" : { \"query\" : [\"Donald\", \"Mike\"], \"path\" : \"Name\" } }", DriverVersions.V2_19_AndHigher)]
+ [BuildersMQL("{ \"regex\" : { \"query\" : \"Alice\", \"path\" : \"Name\" } }", DriverVersions.V2_19_AndHigher, 2, 3)]
+ public void Regex()
+ {
+ _ = Builders.Search.Regex(m => m.Name, new[] { "Donald", "Mike" });
+ _ = Builders.Search.Regex("Name", "Alice");
+ _ = Builders.Search.Regex("Name", "Alice");
+ }
+
+ [BuildersMQL("{ \"span\" : { \"first\" : { \"operator\" : { \"term\" : { \"query\" : \"foo\", \"path\" : \"Name\" } }, \"endPositionLte\" : 5 } } }", DriverVersions.V2_19_AndHigher)]
+ [BuildersMQL("{ \"span\" : { \"or\" : { \"clauses\" : [{ \"term\" : { \"query\" : \"a\", \"path\" : \"Name\" } }, { \"term\" : { \"query\" : \"b\", \"path\" : \"Name\" } }, { \"term\" : { \"query\" : \"c\", \"path\" : \"Name\" } }] } } }", DriverVersions.V2_19_AndHigher)]
+ public void Span()
+ {
+ _ = Builders.Search.Span(Builders.SearchSpan
+ .First(Builders.SearchSpan.Term(p => p.Name, "foo"), 5));
+
+ _ = Builders.Search.Span(Builders.SearchSpan.Or(
+ Builders.SearchSpan.Term(p => p.Name, "a"),
+ Builders.SearchSpan.Term(p => p.Name, "b"),
+ Builders.SearchSpan.Term(p => p.Name, "c")));
+ }
+
+ [BuildersMQL("{ \"text\" : { \"query\" : \"My address\", \"path\" : \"Address\" } }", DriverVersions.V2_19_AndHigher, 1, 2, 3)]
+ public void Text()
+ {
+ _ = Builders.Search.Text(m => m.Address, "My address");
+ _ = Builders.Search.Text("Address", "My address");
+ _ = Builders.Search.Text("Address", "My address");
+ }
+
+ [NoDiagnostics(DriverVersions.V2_19_AndHigher)]
+ public void Valid_but_ignored()
+ {
+ _ = Builders.Search.GeoShape(
+ "location",
+ GeoShapeRelation.Disjoint,
+ s_testPolygon);
+
+ _ = Builders.Search.GeoWithin(
+ "location",
+ s_testPolygon);
+
+ _ = Builders.Search.Range(m => m.SiblingsCount, SearchRangeBuilder.Gt(1).Lt(10));
+ _ = Builders.Search.Near("Date", DateTime.Now, 1);
+
+ _ = Builders.Search.Wildcard(new FieldDefinition[]
+ {
+ new ExpressionFieldDefinition(x => x.Name),
+ new ExpressionFieldDefinition(x => x.LastName)
+ }, "A");
+ }
+
+ [NoDiagnostics(DriverVersions.V2_18_AndLower)]
+ public void Search_should_be_ignored_in_older_drivers()
+ {
+ // Technically Atlas Search code can't appear with older drivers, via normal C# usage,
+ // so this test covers isoteric edge cases.
+ _ = Builders.Search.Text(m => m.Address, "My address");
+ _ = Builders.Search.Phrase(m => m.Address.Province, "Columbia");
+ }
+ }
+}
diff --git a/tests/MongoDB.Analyzer.Tests.Common/DataModel/Person.cs b/tests/MongoDB.Analyzer.Tests.Common/DataModel/Person.cs
index 74075f18..db2e413f 100644
--- a/tests/MongoDB.Analyzer.Tests.Common/DataModel/Person.cs
+++ b/tests/MongoDB.Analyzer.Tests.Common/DataModel/Person.cs
@@ -24,5 +24,7 @@ public class Person
public int SiblingsCount { get; set; }
public long TicksSinceBirth { get; set; }
+
+ public bool IsRetired { get; set; }
}
}
diff --git a/tests/MongoDB.Analyzer.Tests.Common/DiagnosticTestCaseAttribute.cs b/tests/MongoDB.Analyzer.Tests.Common/DiagnosticTestCaseAttribute.cs
index d9c190bc..1c1a4757 100644
--- a/tests/MongoDB.Analyzer.Tests.Common/DiagnosticTestCaseAttribute.cs
+++ b/tests/MongoDB.Analyzer.Tests.Common/DiagnosticTestCaseAttribute.cs
@@ -33,21 +33,21 @@ public DiagnosticRuleTestCaseAttribute(
string version = null,
LinqVersion linqProvider = LinqVersion.V2,
DriverTargetFramework targetFramework = DriverTargetFramework.All,
- Location[] locations = null)
+ int[] codeLines = null)
{
RuleId = ruleId;
Message = message;
Version = version;
LinqProvider = linqProvider;
TargetFramework = targetFramework;
- Locations = locations ?? new[] { Location.Empty };
+ Locations = codeLines?.Any() == true ? codeLines.Select(l => new Location(l, -1)).ToArray() : new[] { Location.Empty };
}
}
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class NoDiagnosticsAttribute : DiagnosticRuleTestCaseAttribute
{
- public NoDiagnosticsAttribute() : base(DiagnosticRulesConstants.NoRule, null) { }
+ public NoDiagnosticsAttribute(string version = null) : base(DiagnosticRulesConstants.NoRule, null, version: version) { }
}
public class MQLAttribute : DiagnosticRuleTestCaseAttribute
@@ -70,7 +70,7 @@ public MQLAttribute(
version,
linqProvider,
targetFramework,
- codeLines.Any() ? codeLines.Select(l => new Location(l, -1)).ToArray() : null)
+ codeLines)
{
}
}
@@ -122,14 +122,15 @@ public sealed class BuildersMQLAttribute : DiagnosticRuleTestCaseAttribute
public BuildersMQLAttribute(string message, params int[] codeLines) :
base(DiagnosticRulesConstants.Builders2MQL,
message,
- locations: codeLines.Any() ? codeLines.Select(l => new Location(l, -1)).ToArray() : null)
+ codeLines: codeLines)
{
}
- public BuildersMQLAttribute(string message, string version) :
+ public BuildersMQLAttribute(string message, string version, params int[] codeLines) :
base(DiagnosticRulesConstants.Builders2MQL,
message,
- version: version)
+ version: version,
+ codeLines: codeLines)
{
}
}
diff --git a/tests/MongoDB.Analyzer.Tests/Builders/BuildersTests.cs b/tests/MongoDB.Analyzer.Tests/Builders/BuildersTests.cs
index 457476d3..7726c410 100644
--- a/tests/MongoDB.Analyzer.Tests/Builders/BuildersTests.cs
+++ b/tests/MongoDB.Analyzer.Tests/Builders/BuildersTests.cs
@@ -30,6 +30,10 @@ public sealed class BuildersTests : DiagnosticsTestCasesRunner
[CodeBasedTestCasesSource(typeof(BuildersArrays))]
public Task Arrays(DiagnosticTestCase testCase) => VerifyTestCase(testCase);
+ [DataTestMethod]
+ [CodeBasedTestCasesSource(typeof(BuildersAtlasSearch))]
+ public Task AtlasSearch(DiagnosticTestCase testCase) => VerifyTestCase(testCase);
+
[DataTestMethod]
[CodeBasedTestCasesSource(typeof(BuildersBasic))]
public Task Basic(DiagnosticTestCase testCase) => VerifyTestCase(testCase);
diff --git a/tests/MongoDB.Analyzer.Tests/MongoDB.Analyzer.Tests.csproj b/tests/MongoDB.Analyzer.Tests/MongoDB.Analyzer.Tests.csproj
index 938f9bb5..c2594508 100644
--- a/tests/MongoDB.Analyzer.Tests/MongoDB.Analyzer.Tests.csproj
+++ b/tests/MongoDB.Analyzer.Tests/MongoDB.Analyzer.Tests.csproj
@@ -18,7 +18,7 @@
-
+
runtime