From bcfe5661cfa3cf59b52edc5d9b9241a14dd7f4fa Mon Sep 17 00:00:00 2001 From: BorisDog Date: Tue, 14 Feb 2023 17:44:31 -0800 Subject: [PATCH 1/2] VS-83: Add Atlas Search basic builders support --- samples/BasicSample/BasicSample.csproj | 2 +- samples/BasicSample/BuildersSample.cs | 13 ++ .../Builders/Renderer.cs | 2 +- .../Builders/Renderer_2_19_and_higher.cs | 28 ++++ .../MongoDB.Analyzer.Helpers.csproj | 2 +- .../Core/Builders/AnalysisCodeGenerator.cs | 7 + .../Builders/BuildersAnalysisConstants.cs | 2 + .../Core/HelperResources/ResourceNames.cs | 1 + .../Core/Utilities/SymbolExtensions.cs | 8 +- ....Analyzer.Tests.Common.ClassLibrary.csproj | 2 +- .../Builders/BuildersAtlasSearch.cs | 150 ++++++++++++++++++ .../DataModel/Person.cs | 2 + .../DiagnosticTestCaseAttribute.cs | 5 +- .../Builders/BuildersTests.cs | 4 + .../MongoDB.Analyzer.Tests.csproj | 2 +- 15 files changed, 220 insertions(+), 10 deletions(-) create mode 100644 src/MongoDB.Analyzer.Helpers/Builders/Renderer_2_19_and_higher.cs create mode 100644 tests/MongoDB.Analyzer.Tests.Common.TestCases/Builders/BuildersAtlasSearch.cs 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..a544a4d4 100644 --- a/tests/MongoDB.Analyzer.Tests.Common/DiagnosticTestCaseAttribute.cs +++ b/tests/MongoDB.Analyzer.Tests.Common/DiagnosticTestCaseAttribute.cs @@ -47,7 +47,7 @@ public DiagnosticRuleTestCaseAttribute( [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 @@ -126,9 +126,10 @@ public BuildersMQLAttribute(string message, params int[] codeLines) : { } - public BuildersMQLAttribute(string message, string version) : + public BuildersMQLAttribute(string message, string version, params int[] codeLines) : base(DiagnosticRulesConstants.Builders2MQL, message, + locations: codeLines.Any() ? codeLines.Select(l => new Location(l, -1)).ToArray() : null, version: version) { } 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 From b6a0456e79da40a3131793d39be6349a8f78d506 Mon Sep 17 00:00:00 2001 From: BorisDog Date: Wed, 15 Feb 2023 09:46:52 -0800 Subject: [PATCH 2/2] - PR comments --- .../DiagnosticTestCaseAttribute.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/MongoDB.Analyzer.Tests.Common/DiagnosticTestCaseAttribute.cs b/tests/MongoDB.Analyzer.Tests.Common/DiagnosticTestCaseAttribute.cs index a544a4d4..1c1a4757 100644 --- a/tests/MongoDB.Analyzer.Tests.Common/DiagnosticTestCaseAttribute.cs +++ b/tests/MongoDB.Analyzer.Tests.Common/DiagnosticTestCaseAttribute.cs @@ -33,14 +33,14 @@ 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 }; } } @@ -70,7 +70,7 @@ public MQLAttribute( version, linqProvider, targetFramework, - codeLines.Any() ? codeLines.Select(l => new Location(l, -1)).ToArray() : null) + codeLines) { } } @@ -122,15 +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, params int[] codeLines) : base(DiagnosticRulesConstants.Builders2MQL, message, - locations: codeLines.Any() ? codeLines.Select(l => new Location(l, -1)).ToArray() : null, - version: version) + version: version, + codeLines: codeLines) { } }