diff --git a/SonarQube.VisualStudio.sln b/SonarQube.VisualStudio.sln index 15071cab56..1eeb518fb7 100644 --- a/SonarQube.VisualStudio.sln +++ b/SonarQube.VisualStudio.sln @@ -22,6 +22,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Directory.Build.props = Directory.Build.props Directory.Build.targets = Directory.Build.targets nuget.config = nuget.config + src\RoslynAnalyzerServer\RoslynAnalyzerServer.csproj = src\RoslynAnalyzerServer\RoslynAnalyzerServer.csproj SonarLint.VsTargetVersion.props = SonarLint.VsTargetVersion.props EndProjectSection EndProject @@ -81,12 +82,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CFamily", "src\CFamily\CFam EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CFamily.UnitTests", "src\CFamily.UnitTests\CFamily.UnitTests.csproj", "{30E1FF8F-94BA-4A39-A737-8FFD7B4A0CD3}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Roslyn.Suppressions", "Roslyn.Suppressions", "{16BF2D77-AE3B-4218-A3E8-875829D73B00}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Roslyn.Suppressions", "src\Roslyn.Suppressions\Roslyn.Suppressions\Roslyn.Suppressions.csproj", "{082D5D8E-F914-4139-9AE3-3F48B679E3DA}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Roslyn.Suppressions.UnitTests", "src\Roslyn.Suppressions\Roslyn.Suppressions.UnitTests\Roslyn.Suppressions.UnitTests.csproj", "{C478DAE7-58BC-4D02-929E-E413B40F2517}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ConnectedMode", "ConnectedMode", "{3B4A8B40-9821-4964-8EAB-1D8A0B078292}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConnectedMode", "src\ConnectedMode\ConnectedMode.csproj", "{0BE551DB-3C46-42A5-BB38-DA80E83F8ABD}" @@ -134,6 +129,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CopyDependencies", "build\C {3ECCAF9D-3B23-4980-83E2-8ACEE0FC6BEB} = {3ECCAF9D-3B23-4980-83E2-8ACEE0FC6BEB} EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RoslynAnalyzerServer", "RoslynAnalyzerServer", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RoslynAnalyzerServer", "src\RoslynAnalyzerServer\RoslynAnalyzerServer.csproj", "{7EDBF3B2-2820-4C35-A368-CE3213F0BBE4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RoslynAnalyzerServer.UnitTests", "src\RoslynAnalyzerServer.UnitTests\RoslynAnalyzerServer.UnitTests.csproj", "{754189A4-1458-4F0C-9D1A-E4F359B66E37}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RoslynAnalyzerServer.IntegrationTests", "src\RoslynAnalyzerServer.IntegrationTests\RoslynAnalyzerServer.IntegrationTests.csproj", "{DCC4771A-C097-4B46-B1EB-39C3E13C6252}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -262,22 +265,6 @@ Global {30E1FF8F-94BA-4A39-A737-8FFD7B4A0CD3}.Release|Any CPU.Build.0 = Release|Any CPU {30E1FF8F-94BA-4A39-A737-8FFD7B4A0CD3}.Release|x86.ActiveCfg = Release|Any CPU {30E1FF8F-94BA-4A39-A737-8FFD7B4A0CD3}.Release|x86.Build.0 = Release|Any CPU - {082D5D8E-F914-4139-9AE3-3F48B679E3DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {082D5D8E-F914-4139-9AE3-3F48B679E3DA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {082D5D8E-F914-4139-9AE3-3F48B679E3DA}.Debug|x86.ActiveCfg = Debug|Any CPU - {082D5D8E-F914-4139-9AE3-3F48B679E3DA}.Debug|x86.Build.0 = Debug|Any CPU - {082D5D8E-F914-4139-9AE3-3F48B679E3DA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {082D5D8E-F914-4139-9AE3-3F48B679E3DA}.Release|Any CPU.Build.0 = Release|Any CPU - {082D5D8E-F914-4139-9AE3-3F48B679E3DA}.Release|x86.ActiveCfg = Release|Any CPU - {082D5D8E-F914-4139-9AE3-3F48B679E3DA}.Release|x86.Build.0 = Release|Any CPU - {C478DAE7-58BC-4D02-929E-E413B40F2517}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C478DAE7-58BC-4D02-929E-E413B40F2517}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C478DAE7-58BC-4D02-929E-E413B40F2517}.Debug|x86.ActiveCfg = Debug|Any CPU - {C478DAE7-58BC-4D02-929E-E413B40F2517}.Debug|x86.Build.0 = Debug|Any CPU - {C478DAE7-58BC-4D02-929E-E413B40F2517}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C478DAE7-58BC-4D02-929E-E413B40F2517}.Release|Any CPU.Build.0 = Release|Any CPU - {C478DAE7-58BC-4D02-929E-E413B40F2517}.Release|x86.ActiveCfg = Release|Any CPU - {C478DAE7-58BC-4D02-929E-E413B40F2517}.Release|x86.Build.0 = Release|Any CPU {0BE551DB-3C46-42A5-BB38-DA80E83F8ABD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0BE551DB-3C46-42A5-BB38-DA80E83F8ABD}.Debug|Any CPU.Build.0 = Debug|Any CPU {0BE551DB-3C46-42A5-BB38-DA80E83F8ABD}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -382,6 +369,30 @@ Global {2BB16C6F-BF06-4225-99A0-F1CFE70CBE44}.Release|Any CPU.Build.0 = Debug|x86 {2BB16C6F-BF06-4225-99A0-F1CFE70CBE44}.Release|x86.ActiveCfg = Debug|x86 {2BB16C6F-BF06-4225-99A0-F1CFE70CBE44}.Release|x86.Build.0 = Debug|x86 + {7EDBF3B2-2820-4C35-A368-CE3213F0BBE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7EDBF3B2-2820-4C35-A368-CE3213F0BBE4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7EDBF3B2-2820-4C35-A368-CE3213F0BBE4}.Debug|x86.ActiveCfg = Debug|Any CPU + {7EDBF3B2-2820-4C35-A368-CE3213F0BBE4}.Debug|x86.Build.0 = Debug|Any CPU + {7EDBF3B2-2820-4C35-A368-CE3213F0BBE4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7EDBF3B2-2820-4C35-A368-CE3213F0BBE4}.Release|Any CPU.Build.0 = Release|Any CPU + {7EDBF3B2-2820-4C35-A368-CE3213F0BBE4}.Release|x86.ActiveCfg = Release|Any CPU + {7EDBF3B2-2820-4C35-A368-CE3213F0BBE4}.Release|x86.Build.0 = Release|Any CPU + {754189A4-1458-4F0C-9D1A-E4F359B66E37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {754189A4-1458-4F0C-9D1A-E4F359B66E37}.Debug|Any CPU.Build.0 = Debug|Any CPU + {754189A4-1458-4F0C-9D1A-E4F359B66E37}.Debug|x86.ActiveCfg = Debug|Any CPU + {754189A4-1458-4F0C-9D1A-E4F359B66E37}.Debug|x86.Build.0 = Debug|Any CPU + {754189A4-1458-4F0C-9D1A-E4F359B66E37}.Release|Any CPU.ActiveCfg = Release|Any CPU + {754189A4-1458-4F0C-9D1A-E4F359B66E37}.Release|Any CPU.Build.0 = Release|Any CPU + {754189A4-1458-4F0C-9D1A-E4F359B66E37}.Release|x86.ActiveCfg = Release|Any CPU + {754189A4-1458-4F0C-9D1A-E4F359B66E37}.Release|x86.Build.0 = Release|Any CPU + {DCC4771A-C097-4B46-B1EB-39C3E13C6252}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DCC4771A-C097-4B46-B1EB-39C3E13C6252}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DCC4771A-C097-4B46-B1EB-39C3E13C6252}.Debug|x86.ActiveCfg = Debug|Any CPU + {DCC4771A-C097-4B46-B1EB-39C3E13C6252}.Debug|x86.Build.0 = Debug|Any CPU + {DCC4771A-C097-4B46-B1EB-39C3E13C6252}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DCC4771A-C097-4B46-B1EB-39C3E13C6252}.Release|Any CPU.Build.0 = Release|Any CPU + {DCC4771A-C097-4B46-B1EB-39C3E13C6252}.Release|x86.ActiveCfg = Release|Any CPU + {DCC4771A-C097-4B46-B1EB-39C3E13C6252}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -404,8 +415,6 @@ Global {407919AF-3AEA-483D-9183-1063EDECDBC3} = {BE58232B-BE46-4D0D-9175-6BE0517F3EF0} {58619C0F-0F3D-4E8C-B204-A19B332D45E5} = {11D4BFC7-C1F2-45AC-888E-25A6A216AD1D} {30E1FF8F-94BA-4A39-A737-8FFD7B4A0CD3} = {11D4BFC7-C1F2-45AC-888E-25A6A216AD1D} - {082D5D8E-F914-4139-9AE3-3F48B679E3DA} = {16BF2D77-AE3B-4218-A3E8-875829D73B00} - {C478DAE7-58BC-4D02-929E-E413B40F2517} = {16BF2D77-AE3B-4218-A3E8-875829D73B00} {0BE551DB-3C46-42A5-BB38-DA80E83F8ABD} = {3B4A8B40-9821-4964-8EAB-1D8A0B078292} {2BD38A3A-6F0E-452B-A5B2-200113A32184} = {3B4A8B40-9821-4964-8EAB-1D8A0B078292} {67BEB251-4EA5-44EE-92A7-B4F57D9A6867} = {25DE7210-DFC0-448B-894E-84C1C9CA223E} @@ -422,6 +431,9 @@ Global {1625398B-2343-481E-9B90-57B38EBEE8C5} = {236587E8-62A7-4E4E-815D-A50433859DC7} {421D8026-2CBF-4444-A886-67428C1813E9} = {E93C2CF9-69A6-4669-BE8A-6060B18FEDCA} {2BB16C6F-BF06-4225-99A0-F1CFE70CBE44} = {421D8026-2CBF-4444-A886-67428C1813E9} + {7EDBF3B2-2820-4C35-A368-CE3213F0BBE4} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {754189A4-1458-4F0C-9D1A-E4F359B66E37} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {DCC4771A-C097-4B46-B1EB-39C3E13C6252} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DABC27C8-E761-4826-AD2D-056F677EF3C0} diff --git a/SonarQube.VisualStudio.sln.DotSettings b/SonarQube.VisualStudio.sln.DotSettings index 1c12bf3bca..5309ade584 100644 --- a/SonarQube.VisualStudio.sln.DotSettings +++ b/SonarQube.VisualStudio.sln.DotSettings @@ -25,6 +25,7 @@ False True CHOP_IF_LONG + True True True CHOP_IF_LONG diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES index 8ad9088d6c..252018b158 100644 --- a/THIRD-PARTY-NOTICES +++ b/THIRD-PARTY-NOTICES @@ -345,46 +345,6 @@ Apache License Version 2.0 END OF TERMS AND CONDITIONS - -License notice for Google Protobuf ------------------------------------------------------ -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -Code generated by the Protocol Buffer compiler is owned by the owner -of the input file used when generating it. This code is not -standalone and requires a support library to be linked with it. This -support library is itself covered by the above license. - -https://github.com/protocolbuffers/protobuf/blob/master/LICENSE - - -License notice for GRPC ------------------------------------------------------ -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. - -https://github.com/grpc/grpc/blob/master/LICENSE - - License notice for Newtonsoft.Json ----------------------------------------------------- The MIT License (MIT) diff --git a/build/CopyDependencies/CopyDependencies.targets b/build/CopyDependencies/CopyDependencies.targets index ed49120aad..167acdec68 100644 --- a/build/CopyDependencies/CopyDependencies.targets +++ b/build/CopyDependencies/CopyDependencies.targets @@ -37,6 +37,7 @@ + diff --git a/build/DownloadDependencies/CommonProperties.props b/build/DownloadDependencies/CommonProperties.props index e112a6389f..ed8603c2eb 100644 --- a/build/DownloadDependencies/CommonProperties.props +++ b/build/DownloadDependencies/CommonProperties.props @@ -58,6 +58,12 @@ $(JarDownloadDir)\$(SonarTextPluginFileName) https://repox.jfrog.io/artifactory/sonarsource/org/sonarsource/text/sonar-text-plugin/$(EmbeddedSonarSecretsJarVersion)/$(SonarTextPluginFileName) + + + sonarqube-ide-visualstudio-roslyn-plugin-$(EmbeddedSonarSqvsRoslynJarVersion).jar + $(JarDownloadDir)\$(SonarSqvsRoslynPluginFileName) + https://repox.jfrog.io/artifactory/sonarsource/org/sonarsource/sonarlint/visualstudio/roslyn/sonarqube-ide-visualstudio-roslyn-plugin/$(EmbeddedSonarSqvsRoslynJarVersion)/$(SonarSqvsRoslynPluginFileName) + $(LOCALAPPDATA)\SLVS_Build_Dotnet diff --git a/build/DownloadDependencies/JarProcessing.targets b/build/DownloadDependencies/JarProcessing.targets index e75dbc19f8..efc113cce7 100644 --- a/build/DownloadDependencies/JarProcessing.targets +++ b/build/DownloadDependencies/JarProcessing.targets @@ -54,6 +54,7 @@ + diff --git a/src/CFamily.UnitTests/packages.lock.json b/src/CFamily.UnitTests/packages.lock.json index 975bb3b8e6..0da00f606f 100644 --- a/src/CFamily.UnitTests/packages.lock.json +++ b/src/CFamily.UnitTests/packages.lock.json @@ -116,16 +116,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -1260,7 +1250,7 @@ "SonarLint.VisualStudio.Integration": "[1.0.0, )", "SonarLint.VisualStudio.IssueVisualization": "[1.0.0, )", "SonarLint.VisualStudio.IssueVisualization.Security": "[1.0.0, )", - "SonarLint.VisualStudio.Roslyn.Suppressions": "[1.0.0, )", + "SonarLint.VisualStudio.RoslynAnalyzerServer": "[1.0.0, )", "SonarLint.VisualStudio.SLCore": "[1.0.0, )", "SonarLint.VisualStudio.SLCore.Listeners": "[1.0.0, )", "SonarQube.Client": "[1.0.0, )", @@ -1376,16 +1366,10 @@ "SonarLint.VisualStudio.IssueVisualization": "[1.0.0, )" } }, - "SonarLint.VisualStudio.Roslyn.Suppressions": { + "SonarLint.VisualStudio.RoslynAnalyzerServer": { "type": "Project", "dependencies": { - "Microsoft.VisualStudio.Sdk": "[17.0.31902.203, )", - "Newtonsoft.Json": "[13.0.3, )", - "SonarLint.VisualStudio.ConnectedMode": "[1.0.0, )", - "SonarLint.VisualStudio.Core": "[1.0.0, )", - "SonarLint.VisualStudio.Infrastructure.VS": "[1.0.0, )", - "SonarQube.Client": "[1.0.0, )", - "System.IO.Abstractions": "[9.0.4, )" + "SonarLint.VisualStudio.Core": "[1.0.0, )" } }, "SonarLint.VisualStudio.SLCore": { @@ -1400,14 +1384,13 @@ "dependencies": { "SonarLint.VisualStudio.ConnectedMode": "[1.0.0, )", "SonarLint.VisualStudio.IssueVisualization.Security": "[1.0.0, )", + "SonarLint.VisualStudio.RoslynAnalyzerServer": "[1.0.0, )", "SonarLint.VisualStudio.SLCore": "[1.0.0, )" } }, "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/ConnectedMode.UnitTests/Binding/BindingProcessFactoryTests.cs b/src/ConnectedMode.UnitTests/Binding/BindingProcessFactoryTests.cs deleted file mode 100644 index 3a4e745ca2..0000000000 --- a/src/ConnectedMode.UnitTests/Binding/BindingProcessFactoryTests.cs +++ /dev/null @@ -1,67 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using SonarLint.VisualStudio.ConnectedMode.Binding; -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Analysis; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Binding -{ - [TestClass] - public class BindingProcessFactoryTests - { - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void Create_ReturnsProcessImpl() - { - var bindingArgs = new BindCommandArgs(new BoundServerProject("any", "any", new ServerConnection.SonarCloud("any"))); - - var testSubject = CreateTestSubject(); - - var actual = testSubject.Create(bindingArgs); - actual.Should().Should().NotBeNull(); - actual.Should().BeOfType(); - } - - private static BindingProcessFactory CreateTestSubject( - IQualityProfileDownloader qualityProfileDownloader = null, - ILogger logger = null) - { - qualityProfileDownloader ??= Mock.Of(); - logger ??= new TestLogger(logToConsole: true); - - return new BindingProcessFactory(qualityProfileDownloader, logger); - } - - } -} diff --git a/src/ConnectedMode.UnitTests/Binding/BindingProcessImplTests.cs b/src/ConnectedMode.UnitTests/Binding/BindingProcessImplTests.cs deleted file mode 100644 index f82f1fd662..0000000000 --- a/src/ConnectedMode.UnitTests/Binding/BindingProcessImplTests.cs +++ /dev/null @@ -1,152 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Threading; -using System.Threading.Tasks; -using SonarLint.VisualStudio.ConnectedMode.Binding; -using SonarLint.VisualStudio.ConnectedMode.Persistence; -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Analysis; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client; -using SonarQube.Client.Helpers; -using System.Security; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Binding -{ - [TestClass] - public class BindingProcessImplTests - { - #region Tests - - [TestMethod] - public void Ctor_ArgChecks() - { - var bindingArgs = CreateBindCommandArgs(); - var qpDownloader = Mock.Of(); - Mock.Of(); - var logger = Mock.Of(); - - // 1. Null binding args - Action act = () => new BindingProcessImpl(null, qpDownloader, logger); - act.Should().ThrowExactly().And.ParamName.Should().Be("bindingArgs"); - - // 2. Null QP downloader - act = () => new BindingProcessImpl(bindingArgs, null, logger); - act.Should().ThrowExactly().And.ParamName.Should().Be("qualityProfileDownloader"); - - // 3. Null logger - act = () => new BindingProcessImpl(bindingArgs, qpDownloader, null); - act.Should().ThrowExactly().And.ParamName.Should().Be("logger"); - } - - [TestMethod] - public async Task DownloadQualityProfile_CreatesBoundProjectAndCallsQPDownloader() - { - var qpDownloader = new Mock(); - var progress = Mock.Of>(); - - var bindingArgs = CreateBindCommandArgs("the project key", "http://theServer"); - - var testSubject = CreateTestSubject(bindingArgs, - qpDownloader: qpDownloader.Object); - - // Act - var result = await testSubject.DownloadQualityProfileAsync(progress, CancellationToken.None); - - result.Should().BeTrue(); - - qpDownloader.Verify(x => x.UpdateAsync(It.IsAny(), progress, It.IsAny()), - Times.Once); - - var actualProject = (BoundServerProject)qpDownloader.Invocations[0].Arguments[0]; - - // Check the bound project was correctly constructed from the BindCommandArgs - actualProject.Should().NotBeNull(); - actualProject.ServerConnection.ServerUri.Should().Be("http://theServer"); - actualProject.ServerProjectKey.Should().Be("the project key"); - } - - [TestMethod] - public async Task DownloadQualityProfile_HandlesInvalidOperationException() - { - var qpDownloader = new Mock(); - qpDownloader - .Setup(x => - x.UpdateAsync(It.IsAny(), - It.IsAny>(), - It.IsAny())) - .Throws(new InvalidOperationException()); - - var testSubject = CreateTestSubject( - qpDownloader: qpDownloader.Object); - - // Act - var result = - await testSubject.DownloadQualityProfileAsync(Mock.Of>(), CancellationToken.None); - - result.Should().BeFalse(); - qpDownloader.Verify(x => x.UpdateAsync(It.IsAny(), - It.IsAny>(), - It.IsAny()), - Times.Once); - } - - #endregion Tests - - #region Helpers - - private BindingProcessImpl CreateTestSubject(BindCommandArgs bindingArgs = null, - IQualityProfileDownloader qpDownloader = null, - ILogger logger = null) - { - bindingArgs = bindingArgs ?? CreateBindCommandArgs(); - qpDownloader ??= Mock.Of(); - logger ??= new TestLogger(logToConsole: true); - - return new BindingProcessImpl(bindingArgs, - qpDownloader, - logger); - } - - private BindCommandArgs CreateBindCommandArgs(string projectKey = "key", string serverUri = "http://any") - { - return new BindCommandArgs(new BoundServerProject("any", projectKey, new ServerConnection.SonarQube(new Uri(serverUri)))); - } - - private static void CheckIsExpectedPassword(string expectedRawPassword, SecureString actualPassword) - { - // The SecureString extension methods in SonarQube.Client.Helpers.SecureStringHelper throw for - // nulls - if (expectedRawPassword == null) - { - actualPassword.Should().BeNull(); - } - else - { - actualPassword.ToUnsecureString().Should().Be(expectedRawPassword); - } - } - #endregion Helpers - } -} diff --git a/src/ConnectedMode.UnitTests/Binding/RoslynBindingConfigProviderTests.cs b/src/ConnectedMode.UnitTests/Binding/RoslynBindingConfigProviderTests.cs deleted file mode 100644 index e3ca19e64f..0000000000 --- a/src/ConnectedMode.UnitTests/Binding/RoslynBindingConfigProviderTests.cs +++ /dev/null @@ -1,268 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.Binding; -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.Core.CSharpVB; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client; -using SonarQube.Client.Models; -using Language = SonarLint.VisualStudio.Core.Language; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Binding; - -[TestClass] -public class RoslynBindingConfigProviderTests -{ - private IList validRules; - private IList anyProperties; - private SonarQubeQualityProfile validQualityProfile; - - private static readonly SonarQubeRule ActiveRuleWithUnsupportedSeverity = new SonarQubeRule("activeHotspot", "any1", - true, SonarQubeIssueSeverity.Blocker, null, null, null, SonarQubeIssueType.SecurityHotspot); - - private static readonly SonarQubeRule InactiveRuleWithUnsupportedSeverity = new SonarQubeRule("inactiveHotspot", "any2", - false, SonarQubeIssueSeverity.Blocker, null, null, null, SonarQubeIssueType.SecurityHotspot); - - private static readonly SonarQubeRule ActiveTaintAnalysisRule = new SonarQubeRule("activeTaint", "roslyn.sonaranalyzer.security.foo", - true, SonarQubeIssueSeverity.Blocker, null, null, null, SonarQubeIssueType.CodeSmell); - - private static readonly SonarQubeRule InactiveTaintAnalysisRule = new SonarQubeRule("inactiveTaint", "roslyn.sonaranalyzer.security.bar", - false, SonarQubeIssueSeverity.Blocker, null, null, null, SonarQubeIssueType.CodeSmell); - - [TestInitialize] - public void TestInitialize() - { - validRules = new List { new SonarQubeRule("key", "repoKey", true, SonarQubeIssueSeverity.Blocker, null, null, null, SonarQubeIssueType.Bug) }; - - anyProperties = Array.Empty(); - - validQualityProfile = new SonarQubeQualityProfile("qpkey1", "qp name", "any", false, DateTime.UtcNow); - } - - [TestMethod] - public void MefCtor_CheckIsExported() => - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - - [TestMethod] - public void MefCtor_CheckIsSingleton() => - MefTestHelpers.CheckIsSingletonMefComponent(); - - [TestMethod] - public void GetRules_UnsupportedLanguage_Throws() - { - var builder = new TestEnvironmentBuilder(validQualityProfile, Language.Cpp); - var testSubject = builder.CreateTestSubject(); - - // Act - Action act = () => testSubject.SaveConfigurationAsync(validQualityProfile, Language.Cpp, BindingConfiguration.Standalone, CancellationToken.None).Wait(); - - // Assert - act.Should().ThrowExactly().And.ParamName.Should().Be("language"); - } - - [TestMethod] - public void IsLanguageSupported() - { - // Arrange - var builder = new TestEnvironmentBuilder(validQualityProfile, Language.Cpp); - var testSubject = builder.CreateTestSubject(); - - // 1. Supported languages - testSubject.IsLanguageSupported(Language.CSharp).Should().BeTrue(); - testSubject.IsLanguageSupported(Language.VBNET).Should().BeTrue(); - - // 2. Not supported - testSubject.IsLanguageSupported(Language.C).Should().BeFalse(); - testSubject.IsLanguageSupported(Language.Cpp).Should().BeFalse(); - - testSubject.IsLanguageSupported(Language.Unknown).Should().BeFalse(); - } - - [TestMethod] - public async Task GetConfig_NoSupportedActiveRules_Throws() - { - // Arrange - var builder = new TestEnvironmentBuilder(validQualityProfile, Language.VBNET) - { - ActiveRulesResponse = new List { ActiveRuleWithUnsupportedSeverity }, InactiveRulesResponse = validRules, PropertiesResponse = anyProperties - }; - var testSubject = builder.CreateTestSubject(); - - // Act - var act = () => testSubject.SaveConfigurationAsync(validQualityProfile, Language.VBNET, builder.BindingConfiguration, CancellationToken.None); - - // Assert - await act.Should().ThrowExactlyAsync().WithMessage(string.Format(QualityProfilesStrings.FailedToCreateBindingConfigForLanguage, Language.VBNET.Name)); - - builder.Logger.AssertOutputStrings(1); - var expectedOutput = string.Format(BindingStrings.SubTextPaddingFormat, - string.Format(BindingStrings.NoSonarAnalyzerActiveRulesForQualityProfile, validQualityProfile.Name, Language.VBNET.Name)); - builder.Logger.AssertOutputStrings(expectedOutput); - builder.RoslynConfigGenerator.DidNotReceiveWithAnyArgs().GenerateAndSaveConfiguration(Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any(), Arg.Any>(), Arg.Any>()); - } - - [TestMethod] - public async Task GetConfig_HasActiveInactiveAndUnsupportedRules_ReturnsValidBindingConfig() - { - // Arrange - const string expectedServerUrl = "http://myhost:123/"; - - var properties = new SonarQubeProperty[] { new("propertyAAA", "111"), new("propertyBBB", "222") }; - - var activeSupportedRule = CreateRule("activeRuleKey", "repoKey1", true); - var activeRules = new[] { activeSupportedRule, ActiveTaintAnalysisRule, ActiveRuleWithUnsupportedSeverity }; - var inactiveSupportedRule = CreateRule("inactiveRuleKey", "repoKey2", false); - var inactiveRules = new[] { inactiveSupportedRule, InactiveTaintAnalysisRule, InactiveRuleWithUnsupportedSeverity }; - - var builder = new TestEnvironmentBuilder(validQualityProfile, Language.CSharp, expectedServerUrl) - { - ActiveRulesResponse = activeRules, InactiveRulesResponse = inactiveRules, PropertiesResponse = properties - }; - - var testSubject = builder.CreateTestSubject(); - - // Act - await testSubject.SaveConfigurationAsync(validQualityProfile, Language.CSharp, builder.BindingConfiguration, CancellationToken.None); - - // Assert - builder.RoslynConfigGenerator - .Received() - .GenerateAndSaveConfiguration( - Language.CSharp, - builder.BindingConfiguration.BindingConfigDirectory, - Arg.Is>(x => x.SequenceEqual(builder.SonarProperties)), - builder.ServerExclusionsResponse, - Arg.Is((IReadOnlyCollection x) => - x.Select(y => y.Key).SequenceEqual(new []{activeSupportedRule.Key, inactiveSupportedRule.Key})), - Arg.Is>(x => x.SequenceEqual(new []{activeSupportedRule}))); - builder.Logger.AssertOutputStrings(0); // not expecting anything in the case of success - } - - [TestMethod] - [DataRow("roslyn.sonaranalyzer.security.cs", false)] - [DataRow("roslyn.sonaranalyzer.security.vb", false)] - [DataRow("ROSLYN.SONARANALYZER.SECURITY.X", false)] - [DataRow("roslyn.wintellect", true)] - [DataRow("sonaranalyzer-cs", true)] - [DataRow("sonaranalyzer-vbnet", true)] - public void IsSupportedRule_TaintRules(string repositoryKey, bool expected) - { - var rule = CreateRule("any", repositoryKey, true); - - RoslynBindingConfigProvider.IsSupportedRule(rule).Should().Be(expected); - } - - [TestMethod] - [DataRow(SonarQubeIssueType.Unknown, false)] - [DataRow(SonarQubeIssueType.SecurityHotspot, false)] - [DataRow(SonarQubeIssueType.CodeSmell, true)] - [DataRow(SonarQubeIssueType.Bug, true)] - [DataRow(SonarQubeIssueType.Vulnerability, true)] - public void IsSupportedRule_Severity(SonarQubeIssueType issueType, bool expected) - { - var rule = new SonarQubeRule("any", "any", true, SonarQubeIssueSeverity.Blocker, null, null, null, issueType); - - RoslynBindingConfigProvider.IsSupportedRule(rule).Should().Be(expected); - } - - private static SonarQubeRule CreateRule(string ruleKey, string repoKey, bool isActive) => - new SonarQubeRule(ruleKey, repoKey, isActive, SonarQubeIssueSeverity.Blocker, null, null, null, SonarQubeIssueType.CodeSmell); - - private class TestEnvironmentBuilder - { - private Mock sonarQubeServiceMock; - - private readonly SonarQubeQualityProfile profile; - private readonly Language language; - private readonly string serverUrl; - - private const string ExpectedProjectKey = "fixed.project.key"; - - public TestEnvironmentBuilder(SonarQubeQualityProfile profile, Language language, string serverUrl = "http://any") - { - this.profile = profile; - this.language = language; - this.serverUrl = serverUrl; - - Logger = new TestLogger(); - PropertiesResponse = new List(); - } - - public BindingConfiguration BindingConfiguration { get; private set; } - public IRoslynConfigGenerator RoslynConfigGenerator { get; private set; } - public IList ActiveRulesResponse { get; set; } - public IList InactiveRulesResponse { get; set; } - public IList PropertiesResponse { get; set; } - - public ServerExclusions ServerExclusionsResponse { get; set; } - public Dictionary SonarProperties { get; set; } - public TestLogger Logger { get; private set; } - - public RoslynBindingConfigProvider CreateTestSubject() - { - // Note: where possible, the mocked methods are set up with the expected - // parameter values i.e. they will only be called if the correct values - // are passed in. - Logger = new TestLogger(); - - sonarQubeServiceMock = new Mock(); - sonarQubeServiceMock - .Setup(x => x.GetRulesAsync(true, profile.Key, It.IsAny())) - .ReturnsAsync(ActiveRulesResponse); - - sonarQubeServiceMock - .Setup(x => x.GetRulesAsync(false, profile.Key, It.IsAny())) - .ReturnsAsync(InactiveRulesResponse); - - sonarQubeServiceMock - .Setup(x => x.GetAllPropertiesAsync(ExpectedProjectKey, It.IsAny())) - .ReturnsAsync(PropertiesResponse); - - ServerExclusionsResponse = new ServerExclusions( - exclusions: ["path1"], - globalExclusions: ["path2"], - inclusions: ["path3"]); - - sonarQubeServiceMock - .Setup(x => x.GetServerExclusions(ExpectedProjectKey, It.IsAny())) - .ReturnsAsync(ServerExclusionsResponse); - - BindingConfiguration = new BindingConfiguration( - new BoundServerProject("solution", ExpectedProjectKey, new ServerConnection.SonarQube(new Uri(serverUrl))), - SonarLintMode.Connected, - "c:\\test\\"); - - SonarProperties = PropertiesResponse.ToDictionary(x => x.Key, y => y.Value); - - RoslynConfigGenerator = Substitute.For(); - - return new RoslynBindingConfigProvider(sonarQubeServiceMock.Object, Logger, - RoslynConfigGenerator, - LanguageProvider.Instance); - } - } -} diff --git a/src/ConnectedMode.UnitTests/Binding/SonarQubeRoslynRuleStatusTests.cs b/src/ConnectedMode.UnitTests/Binding/SonarQubeRoslynRuleStatusTests.cs deleted file mode 100644 index 81fce6817c..0000000000 --- a/src/ConnectedMode.UnitTests/Binding/SonarQubeRoslynRuleStatusTests.cs +++ /dev/null @@ -1,195 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.Binding; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.CSharpVB; -using SonarQube.Client.Models; -using ArgumentOutOfRangeException = System.ArgumentOutOfRangeException; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Binding; - -[TestClass] -public class SonarQubeRoslynRuleStatusTests -{ - private IEnvironmentSettings environmentSettings; - - [TestInitialize] - public void TestInitialize() - { - environmentSettings = Substitute.For(); - } - - [TestMethod] - [DataRow(SonarQubeIssueSeverity.Info, RuleAction.Info)] - [DataRow(SonarQubeIssueSeverity.Minor, RuleAction.Info)] - [DataRow(SonarQubeIssueSeverity.Major, RuleAction.Warning)] - [DataRow(SonarQubeIssueSeverity.Critical, RuleAction.Warning)] - public void GetVSSeverity_NotBlocker_CorrectlyMapped(SonarQubeIssueSeverity sqSeverity, RuleAction expectedVsSeverity) - { - var testSubject = new SonarQubeRoslynRuleStatus(CreateStandardRule(sqSeverity), environmentSettings); - - testSubject.GetSeverity().Should().Be(expectedVsSeverity); - } - - [TestMethod] - [DataRow(true, RuleAction.Error)] - [DataRow(false, RuleAction.Warning)] - public void GetVSSeverity_Blocker_CorrectlyMapped(bool shouldTreatBlockerAsError, RuleAction expectedVsSeverity) - { - environmentSettings.TreatBlockerSeverityAsError().Returns(shouldTreatBlockerAsError); - - var testSubject = new SonarQubeRoslynRuleStatus(CreateStandardRule(SonarQubeIssueSeverity.Blocker), environmentSettings); - - testSubject.GetSeverity().Should().Be(expectedVsSeverity); - } - - [TestMethod] - [DataRow(SonarQubeIssueSeverity.Unknown)] - [DataRow((SonarQubeIssueSeverity)(-1))] - public void GetVSSeverity_Invalid_Throws(SonarQubeIssueSeverity sqSeverity) - { - var testSubject = new SonarQubeRoslynRuleStatus(CreateStandardRule(sqSeverity), environmentSettings); - - Action act = () => testSubject.GetSeverity(); - - act.Should().Throw(); - } - - [DataTestMethod] - [DataRow(SonarQubeSoftwareQualitySeverity.Info, RuleAction.Info)] - [DataRow(SonarQubeSoftwareQualitySeverity.Low, RuleAction.Info)] - [DataRow(SonarQubeSoftwareQualitySeverity.Medium, RuleAction.Warning)] - public void GetVSSeverity_FromSoftwareQualitySeverity_NotBlocker_CorrectlyMapped(SonarQubeSoftwareQualitySeverity sqSeverity, RuleAction expectedVsSeverity) - { - var testSubject = new SonarQubeRoslynRuleStatus( - CreateMqrRule(sqSeverity), - environmentSettings); - - testSubject.GetSeverity().Should().Be(expectedVsSeverity); - } - - [TestMethod] - [DataRow(SonarQubeSoftwareQualitySeverity.High, true, RuleAction.Error)] - [DataRow(SonarQubeSoftwareQualitySeverity.High, false, RuleAction.Warning)] - [DataRow(SonarQubeSoftwareQualitySeverity.Blocker, true, RuleAction.Error)] - [DataRow(SonarQubeSoftwareQualitySeverity.Blocker, false, RuleAction.Warning)] - public void GetVSSeverity_FromSoftwareQualitySeverity_Blocker_CorrectlyMapped(SonarQubeSoftwareQualitySeverity sqSeverity, bool shouldTreatBlockerAsError, RuleAction expectedVsSeverity) - { - environmentSettings.TreatBlockerSeverityAsError().Returns(shouldTreatBlockerAsError); - - var testSubject = new SonarQubeRoslynRuleStatus(CreateMqrRule(sqSeverity), environmentSettings); - - testSubject.GetSeverity().Should().Be(expectedVsSeverity); - } - - [TestMethod] - public void GetVSSeverity_FromSoftwareQualitySeverity_Invalid_Throws() - { - var testSubject = new SonarQubeRoslynRuleStatus(CreateMqrRule((SonarQubeSoftwareQualitySeverity)(-1)), environmentSettings); - - Action act = () => testSubject.GetSeverity(); - - act.Should().Throw(); - } - - [DynamicData(nameof(MultipleMqrSeveritiesAndHighestConvertedVsSeverity))] - [DataTestMethod] - public void GetVSSeverity_FromSoftwareQualitySeverity_Multiple_TakesHighest(SonarQubeSoftwareQualitySeverity[] severities, RuleAction expectedAction) - { - var testSubject = new SonarQubeRoslynRuleStatus(CreateMqrRule(severities), environmentSettings); - - testSubject.GetSeverity().Should().Be(expectedAction); - } - - [DataRow(SonarQubeIssueSeverity.Info, RuleAction.Info)] - [DataRow(SonarQubeIssueSeverity.Minor, RuleAction.Info)] - [DataRow(SonarQubeIssueSeverity.Major, RuleAction.Warning)] - [DataRow(SonarQubeIssueSeverity.Critical, RuleAction.Warning)] - [DataTestMethod] - public void GetVSSeverity_EmptyMqrSeverities_UsesStandardSeverity(SonarQubeIssueSeverity sqSeverity, RuleAction expectedVsSeverity) - { - var testSubject = new SonarQubeRoslynRuleStatus(CreateRule(new(), sqSeverity), environmentSettings); - - testSubject.GetSeverity().Should().Be(expectedVsSeverity); - } - - [DynamicData(nameof(AllMqrSeverities))] - [DataTestMethod] - public void GetVSSeverity_HasSoftwareQualitySeverity_Inactive_ReturnsNone(SonarQubeSoftwareQualitySeverity severities) - { - var testSubject = new SonarQubeRoslynRuleStatus(CreateMqrRule(isActive: false, severities), environmentSettings); - - testSubject.GetSeverity().Should().Be(RuleAction.None); - } - - [DynamicData(nameof(AllSeverities))] - [DataTestMethod] - public void GetVSSeverity_HasSeverity_Inactive_ReturnsNone(SonarQubeIssueSeverity severity) - { - var testSubject = new SonarQubeRoslynRuleStatus(CreateStandardRule(severity, isActive: false), environmentSettings); - - testSubject.GetSeverity().Should().Be(RuleAction.None); - } - - public static object[][] MultipleMqrSeveritiesAndHighestConvertedVsSeverity => - [ - [new[] { SonarQubeSoftwareQualitySeverity.Blocker, SonarQubeSoftwareQualitySeverity.Low }, RuleAction.Warning], - [new[] { SonarQubeSoftwareQualitySeverity.Low, SonarQubeSoftwareQualitySeverity.Blocker }, RuleAction.Warning], - [new[] { SonarQubeSoftwareQualitySeverity.Blocker, SonarQubeSoftwareQualitySeverity.Blocker }, RuleAction.Warning], - [new[] { SonarQubeSoftwareQualitySeverity.Low, SonarQubeSoftwareQualitySeverity.Info }, RuleAction.Info], - [new[] { SonarQubeSoftwareQualitySeverity.Low, SonarQubeSoftwareQualitySeverity.Info, SonarQubeSoftwareQualitySeverity.High }, RuleAction.Warning], - ]; - - public static object[][] AllMqrSeverities => - Enum.GetValues(typeof(SonarQubeSoftwareQualitySeverity)).Cast() - .Select(severity => new[] { severity }) - .ToArray(); - - public static object[][] AllSeverities => - Enum.GetValues(typeof(SonarQubeIssueSeverity)).Cast() - .Select(severity => new[] { severity }) - .ToArray(); - - private static SonarQubeRule CreateMqrRule(params SonarQubeSoftwareQualitySeverity[] mqrSeverities) => CreateMqrRule(isActive: true, mqrSeverities); - - private static SonarQubeRule CreateMqrRule(bool isActive, params SonarQubeSoftwareQualitySeverity[] mqrSeverities) - { - mqrSeverities.Should().NotBeEmpty(); - var sonarQubeSoftwareQualitySeverities = - Enum.GetValues(typeof(SonarQubeSoftwareQuality)) - .Cast() - .Zip(mqrSeverities, (x, y) => (x, y)) - .ToDictionary(k => k.x, v => v.y); - return CreateRule(sonarQubeSoftwareQualitySeverities, SonarQubeIssueSeverity.Blocker, isActive: isActive); - } - - private static SonarQubeRule CreateStandardRule(SonarQubeIssueSeverity severity, bool isActive = true) => CreateRule(null, severity, isActive); - - private static SonarQubeRule CreateRule(Dictionary mqrSeverity, SonarQubeIssueSeverity severity, bool isActive = true) => - new(default, - default, - isActive, - severity, - default, - mqrSeverity, - default, - default); -} diff --git a/src/ConnectedMode.UnitTests/Binding/UnintrusiveBindingControllerTests.cs b/src/ConnectedMode.UnitTests/Binding/UnintrusiveBindingControllerTests.cs index ebefc0d3ce..a5f24694aa 100644 --- a/src/ConnectedMode.UnitTests/Binding/UnintrusiveBindingControllerTests.cs +++ b/src/ConnectedMode.UnitTests/Binding/UnintrusiveBindingControllerTests.cs @@ -37,40 +37,32 @@ public class UnintrusiveBindingControllerTests private static readonly UsernameAndPasswordCredentials ValidToken = new("TOKEN", new SecureString()); private static readonly BoundServerProject AnyBoundProject = new("any", "any", new ServerConnection.SonarCloud("any", credentials: ValidToken)); private IActiveSolutionChangedHandler activeSolutionChangedHandler; - private IBindingProcess bindingProcess; - private IBindingProcessFactory bindingProcessFactory; private ISonarQubeService sonarQubeService; private UnintrusiveBindingController testSubject; private ISolutionBindingRepository solutionBindingRepository; + private IConfigurationPersister configurationPersister; [TestInitialize] public void TestInitialize() { - CreateBindingProcessFactory(); sonarQubeService = Substitute.For(); activeSolutionChangedHandler = Substitute.For(); solutionBindingRepository = Substitute.For(); - testSubject = new UnintrusiveBindingController(bindingProcessFactory, sonarQubeService, activeSolutionChangedHandler, solutionBindingRepository); + configurationPersister = Substitute.For(); + testSubject = new UnintrusiveBindingController(sonarQubeService, activeSolutionChangedHandler, solutionBindingRepository, configurationPersister); } [TestMethod] public void MefCtor_CheckTypeIsNonShared() => MefTestHelpers.CheckIsNonSharedMefComponent(); - [TestMethod] - public void MefCtor_IUnintrusiveBindingController_CheckIsExported() => - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - [TestMethod] public void MefCtor_IBindingController_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport() + ); [TestMethod] public async Task BindAsync_EstablishesConnection() @@ -102,17 +94,13 @@ public async Task BindAsync_NotifiesBindingChanged() } [TestMethod] - public async Task BindAsync_CallsBindingProcessInOrder() + public async Task BindAsync_PersistsBindingInformation() { var cancellationToken = CancellationToken.None; - await testSubject.BindAsync(AnyBoundProject, null, cancellationToken); + await testSubject.BindAsync(AnyBoundProject, cancellationToken); - Received.InOrder(() => - { - bindingProcessFactory.Create(Arg.Is(b => b.ProjectToBind == AnyBoundProject)); - bindingProcess.DownloadQualityProfileAsync(null, cancellationToken); - }); + configurationPersister.Received(1).Persist(AnyBoundProject); } [TestMethod] @@ -152,12 +140,4 @@ public void Unbind_ReturnsResultOfDeletedBinding(bool expectedResult) result.Should().Be(expectedResult); } - - private void CreateBindingProcessFactory() - { - bindingProcess ??= Substitute.For(); - - bindingProcessFactory = Substitute.For(); - bindingProcessFactory.Create(Arg.Any()).Returns(bindingProcess); - } } diff --git a/src/ConnectedMode.UnitTests/BoundSolutionUpdateHandlerTests.cs b/src/ConnectedMode.UnitTests/BoundSolutionUpdateHandlerTests.cs deleted file mode 100644 index 7f9f091c53..0000000000 --- a/src/ConnectedMode.UnitTests/BoundSolutionUpdateHandlerTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.TestInfrastructure; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests -{ - [TestClass] - public class BoundSolutionUpdateHandlerTests - { - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void MefCtor_CheckIsSingleton() - { - MefTestHelpers.CheckIsSingletonMefComponent(); - } - - [TestMethod] - public void Ctor_SubscribesToEvents() - { - var activeSolutionTracker = new Mock(); - - _ = new BoundSolutionUpdateHandler(activeSolutionTracker.Object, Mock.Of(), Mock.Of()); - - activeSolutionTracker.VerifyAdd(x => x.SolutionBindingChanged += It.IsAny>(), Times.Once); - activeSolutionTracker.VerifyAdd(x => x.SolutionBindingUpdated += It.IsAny(), Times.Once); - } - - [TestMethod] - public void InvokeEvents_ServerStoreUpdatersAreCalled() - { - var activeSolutionTracker = new Mock(); - var suppressionUpdater = new Mock(); - var qualityProfileUpdater = new Mock(); - - _ = new BoundSolutionUpdateHandler(activeSolutionTracker.Object, suppressionUpdater.Object, qualityProfileUpdater.Object); - - activeSolutionTracker.Raise(x => x.SolutionBindingChanged += null, new ActiveSolutionBindingEventArgs(BindingConfiguration.Standalone)); - suppressionUpdater.Verify(x => x.UpdateAllServerSuppressionsAsync(), Times.Once); - qualityProfileUpdater.Verify(x => x.UpdateAsync(), Times.Once); - - activeSolutionTracker.Raise(x => x.SolutionBindingUpdated += null, EventArgs.Empty); - suppressionUpdater.Verify(x => x.UpdateAllServerSuppressionsAsync(), Times.Exactly(2)); - qualityProfileUpdater.Verify(x => x.UpdateAsync(), Times.Exactly(2)); - } - - [TestMethod] - public void Dispose_UnsubscribesToEvent() - { - var activeSolutionTracker = new Mock(); - - var testSubject = new BoundSolutionUpdateHandler(activeSolutionTracker.Object, Mock.Of(), Mock.Of()); - - testSubject.Dispose(); - - activeSolutionTracker.VerifyRemove(x => x.SolutionBindingChanged -= It.IsAny>(), Times.Once); - activeSolutionTracker.VerifyRemove(x => x.SolutionBindingUpdated -= It.IsAny(), Times.Once); - } - } -} diff --git a/src/ConnectedMode.UnitTests/BranchMatcherTests.cs b/src/ConnectedMode.UnitTests/BranchMatcherTests.cs index cb7eced55c..b50f7de265 100644 --- a/src/ConnectedMode.UnitTests/BranchMatcherTests.cs +++ b/src/ConnectedMode.UnitTests/BranchMatcherTests.cs @@ -18,17 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using LibGit2Sharp; using SonarLint.VisualStudio.ConnectedMode.UnitTests.LibGit2SharpWrappers; using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.SLCore.Listener.Branch; using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client; -using SonarQube.Client.Models; namespace SonarLint.VisualStudio.ConnectedMode.UnitTests { @@ -39,7 +33,6 @@ public class BranchMatcherTests public void MefCtor_CheckIsExported() { MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); } @@ -47,26 +40,26 @@ public void MefCtor_CheckIsExported() [DataRow("branch")] [DataRow("Branch")] [TestMethod] - public async Task GetMatchingBranch_ChooseBranchWithSameName(string serverBranchName) + public void GetMatchingBranch_ChooseBranchWithSameName(string serverBranchName) { - var service = CreateSonarQubeService("master", serverBranchName).Object; + var remoteBranches = CreateRemoteBranches("master", serverBranchName); var masterBranch = CreateBranch("master"); var headBranch = CreateBranch("branch"); var repo = CreateRepo(headBranch, masterBranch); - var testSubject = CreateTestSubject(service); + var testSubject = CreateTestSubject(); - var result = await testSubject.GetMatchingBranch("projectKey", repo, CancellationToken.None); + var result = testSubject.GetMatchingBranch("projectKey", repo, remoteBranches); result.Should().Be("branch"); } [TestMethod] - public async Task GetMatchingBranch_NoBranchWithSameName_ChooseClosestMatch() + public void GetMatchingBranch_NoBranchWithSameName_ChooseClosestMatch() { - var service = CreateSonarQubeService("master", "dev").Object; + var remoteBranches = CreateRemoteBranches("master", "dev"); var masterCommit = new CommitWrapper(1); var devBranchCommit = new CommitWrapper(2); @@ -78,17 +71,17 @@ public async Task GetMatchingBranch_NoBranchWithSameName_ChooseClosestMatch() var repo = CreateRepo(headBranch, masterBranch, devBranch); - var testSubject = CreateTestSubject(service); + var testSubject = CreateTestSubject(); - var result = await testSubject.GetMatchingBranch("projectKey", repo, CancellationToken.None); + var result = testSubject.GetMatchingBranch("projectKey", repo, remoteBranches); result.Should().Be("dev"); } [TestMethod] - public async Task GetMatchingBranch_ShorterPathFoundBefore_EarlyOut() + public void GetMatchingBranch_ShorterPathFoundBefore_EarlyOut() { - var service = CreateSonarQubeService("master", "serverbranch").Object; + var remoteBranches = CreateRemoteBranches("master", "serverbranch"); var masterCommit0 = new CommitWrapper(10); var masterCommit1 = new CommitWrapper(11); @@ -106,9 +99,9 @@ public async Task GetMatchingBranch_ShorterPathFoundBefore_EarlyOut() var repo = CreateRepo(localBranch, serverBranch, masterBranch); - var testSubject = CreateTestSubject(service); + var testSubject = CreateTestSubject(); - var result = await testSubject.GetMatchingBranch("projectKey", repo, CancellationToken.None); + var result = testSubject.GetMatchingBranch("projectKey", repo, remoteBranches); result.Should().Be("serverbranch"); @@ -117,7 +110,7 @@ public async Task GetMatchingBranch_ShorterPathFoundBefore_EarlyOut() // * 7 = localbranchCommit2 -> serverBranchCommit3, serverBranchCommit2, serverBranchCommit1, masterCommit3, masterCommit2, masterCommit1, masterCommit0 // * 7 = localbranchCommit1 -> serverBranchCommit3, serverBranchCommit2, serverBranchCommit1, masterCommit3, masterCommit2, masterCommit1, masterCommit0 // * 1 = serverBranchCommit3 -> serverBranchCommit3 - // Total = 7 + 7 + 1 = 13, and closestMatch = 2 + // Total = 7 + 7 + 1 = 15, and closestMatch = 2 ((CommitLogWrapperWithEnumerationCount)serverBranch.Commits).EnumerateCount.Should().Be(15); // ClosestDistance is now 2 so any tries passes those should fail @@ -128,9 +121,9 @@ public async Task GetMatchingBranch_ShorterPathFoundBefore_EarlyOut() } [TestMethod] - public async Task GetMatchingBranch_ClosestBranchNotOnTheServer_IgnoreClosest() + public void GetMatchingBranch_ClosestBranchNotOnTheServer_IgnoreClosest() { - var service = CreateSonarQubeService("master", "dev").Object; + var remoteBranches = CreateRemoteBranches("master", "dev"); var masterCommit = new CommitWrapper(1); var devBranchCommit = new CommitWrapper(2); @@ -144,9 +137,9 @@ public async Task GetMatchingBranch_ClosestBranchNotOnTheServer_IgnoreClosest() var repo = CreateRepo(headBranch, closestBranch, masterBranch, devBranch); - var testSubject = CreateTestSubject(service); + var testSubject = CreateTestSubject(); - var result = await testSubject.GetMatchingBranch("projectKey", repo, CancellationToken.None); + var result = testSubject.GetMatchingBranch("projectKey", repo, remoteBranches); result.Should().Be("dev"); } @@ -185,9 +178,9 @@ public async Task GetMatchingBranch_ClosestBranchNotOnTheServer_IgnoreClosest() */ [TestMethod] - public async Task GetMatchingBranch_MultipleCandidate_ChooseClosestMatch() + public void GetMatchingBranch_MultipleCandidate_ChooseClosestMatch() { - var service = CreateSonarQubeService("master", "Branch1", "Branch2").Object; + var remoteBranches = CreateRemoteBranches("master", "Branch1", "Branch2"); var commit1 = new CommitWrapper(1); var commit2 = new CommitWrapper(2); @@ -207,17 +200,17 @@ public async Task GetMatchingBranch_MultipleCandidate_ChooseClosestMatch() var repo = CreateRepo(headBranch: branch3, masterBranch, branch1, branch2); - var testSubject = CreateTestSubject(service); + var testSubject = CreateTestSubject(); - var result = await testSubject.GetMatchingBranch("projectKey", repo, CancellationToken.None); + var result = testSubject.GetMatchingBranch("projectKey", repo, remoteBranches); result.Should().Be("branch2"); } [TestMethod] - public async Task GetMatchingBranch_NoMatchingBranch_ChooseMain() + public void GetMatchingBranch_NoMatchingBranch_ChooseMain() { - var service = CreateSonarQubeService("premier", "branch1", "branch2").Object; + var remoteBranches = CreateRemoteBranches("premier", "branch1", "branch2"); var masterCommit = new CommitWrapper(1); var branch1Commit = new CommitWrapper(2); @@ -231,17 +224,17 @@ public async Task GetMatchingBranch_NoMatchingBranch_ChooseMain() var repo = CreateRepo(headBranch, masterBranch, branch1, branch2); - var testSubject = CreateTestSubject(service); + var testSubject = CreateTestSubject(); - var result = await testSubject.GetMatchingBranch("projectKey", repo, CancellationToken.None); + var result = testSubject.GetMatchingBranch("projectKey", repo, remoteBranches); result.Should().Be("premier"); } [TestMethod] - public async Task GetMatchingBranch_MultipleMatchingBranchesWithMain_ChooseMain() + public void GetMatchingBranch_MultipleMatchingBranchesWithMain_ChooseMain() { - var service = CreateSonarQubeService("master", "branch1", "branch2").Object; + var remoteBranches = CreateRemoteBranches("master", "branch1", "branch2"); var commit1 = new CommitWrapper(1); var commit2 = new CommitWrapper(2); @@ -256,17 +249,24 @@ public async Task GetMatchingBranch_MultipleMatchingBranchesWithMain_ChooseMain( var repo = CreateRepo(branch3, branch1, branch2, masterBranch); - var testSubject = CreateTestSubject(service); + var testSubject = CreateTestSubject(); - var result = await testSubject.GetMatchingBranch("projectKey", repo, CancellationToken.None); + var result = testSubject.GetMatchingBranch("projectKey", repo, remoteBranches); result.Should().Be("master"); } [TestMethod] - public async Task GetMatchingBranch_HasShortLivedBranches_IgnoreShortLivedBranch() + public void GetMatchingBranch_HasShortLivedBranches_BranchTypeIsIgnored() { - var service = CreateSonarQubeServiceWithTypes("master", ("branch1", "LONG"), ("branch2", "SHORT")).Object; + // Note: the new RemoteBranch doesn't have a branch type, so we're testing that the algorithm + // works correctly without considering branch types + var remoteBranches = new List + { + new RemoteBranch("master", true), + new RemoteBranch("branch1", false), + new RemoteBranch("branch2", false) + }; var commit1 = new CommitWrapper(1); var commit2 = new CommitWrapper(2); @@ -280,41 +280,23 @@ public async Task GetMatchingBranch_HasShortLivedBranches_IgnoreShortLivedBranch var repo = CreateRepo(branch3, masterBranch, branch1, branch2); - var testSubject = CreateTestSubject(service); + var testSubject = CreateTestSubject(); - var result = await testSubject.GetMatchingBranch("projectKey", repo, CancellationToken.None); + var result = testSubject.GetMatchingBranch("projectKey", repo, remoteBranches); - result.Should().Be("branch1"); + result.Should().Be("branch2"); } [TestMethod] - public async Task GetMatchingBranch_RepoHasNoHead_ReturnsNull() + public void GetMatchingBranch_RepoHasNoHead_ReturnsNull() { - var testSubject = CreateTestSubject(CreateSonarQubeService("any").Object); + var testSubject = CreateTestSubject(); - var result = await testSubject.GetMatchingBranch("projectKey", Mock.Of(), CancellationToken.None); + var result = testSubject.GetMatchingBranch("projectKey", Mock.Of(), new List { new RemoteBranch("any", true) }); result.Should().BeNull(); } - [TestMethod] - public async Task GetMatchingBranch_CancellationToken_IsPropagatedToWebClient() - { - var service = CreateSonarQubeService("any"); - - var commit = new CommitWrapper(1); - var branch = CreateBranch("premier", commit); - var repo = CreateRepo(branch); - - var source = new CancellationTokenSource(); - - var testSubject = CreateTestSubject(service.Object); - - _ = await testSubject.GetMatchingBranch("projectKey", repo, source.Token); - - service.Verify(x => x.GetProjectBranchesAsync("projectKey", source.Token), Times.Once); - } - private static IRepository CreateRepo(Branch headBranch, params Branch[] branches) { var branchList = new List @@ -339,33 +321,21 @@ private static BranchWrapper CreateBranch(string branchName, params CommitWrappe private static BranchWrapper CreateBranchWithEnumerationCount(string branchName, params CommitWrapper[] commits) => new(branchName, new CommitLogWrapperWithEnumerationCount(commits)); - private static Mock CreateSonarQubeService(string mainBranch, params string[] branches) + private static List CreateRemoteBranches(string mainBranch, params string[] branches) { - var branchesWithType = branches.Select(b => (b, "BRANCH")).ToArray(); - - return CreateSonarQubeServiceWithTypes(mainBranch, branchesWithType); - } - - private static Mock CreateSonarQubeServiceWithTypes(string mainBranch, params (string branchName, string type)[] branches) - { - var service = new Mock(); - service.Setup(x => x.IsConnected).Returns(true); - - IList remoteBranches = new List(); + var result = new List(); foreach (var branch in branches) { - remoteBranches.Add(new SonarQubeProjectBranch(branch.branchName, false, DateTime.Now, branch.type)); + result.Add(new RemoteBranch(branch, false)); } - remoteBranches.Add(new SonarQubeProjectBranch(mainBranch, true, DateTime.Now, "BRANCH")); - - service.Setup(s => s.GetProjectBranchesAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(remoteBranches)); + result.Add(new RemoteBranch(mainBranch, true)); - return service; + return result; } - private static BranchMatcher CreateTestSubject(ISonarQubeService sonarQubeService) - => new BranchMatcher(sonarQubeService, new TestLogger(logToConsole: true)); + private static BranchMatcher CreateTestSubject() + => new BranchMatcher(new TestLogger(logToConsole: true)); } } diff --git a/src/ConnectedMode.UnitTests/IssueMatcherTests.cs b/src/ConnectedMode.UnitTests/IssueMatcherTests.cs deleted file mode 100644 index b3d77f46a8..0000000000 --- a/src/ConnectedMode.UnitTests/IssueMatcherTests.cs +++ /dev/null @@ -1,193 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using SonarLint.VisualStudio.Core.Suppressions; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests; - -[TestClass] -public class IssueMatcherTests -{ - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported(); - } - - [TestMethod] - public void MefCtor_CheckIsSingleton() - { - MefTestHelpers.CheckIsSingletonMefComponent(); - } - - [DataTestMethod] - [DataRow("CorrectRuleId", 1, "CorrectHash", true)] // exact matches - [DataRow("correctRULEID", 1, "CorrectHash", true)] // rule-id is case-insensitive - [DataRow("CorrectRuleId", 1, "wrong hash", true)] // matches on line - [DataRow("CorrectRuleId", 9999, "CorrectHash", true)] // matches on hash only - [DataRow("CorrectRuleId", 2, "correcthash", false)] // hash is case-sensitive - [DataRow("CorrectRuleId", 2, "wrong hash", false)] // wrong line and hash - [DataRow("CorrectRuleId", null, null, false)] // server file issue - [DataRow("wrong rule Id", 1, "CorrectHash", false)] - public void IsMatch_MatchesBasedOnAllParameters(string serverRuleId, int? serverIssueLine, - string serverHash, bool expectedResult) - { - var issueToMatch = CreateIssueToMatch("CorrectRuleId", 1, "CorrectHash"); - var serverIssue = CreateServerIssue(serverRuleId, serverIssueLine, serverHash); - - CreateTestSubject().IsLikelyMatch(issueToMatch, serverIssue).Should().Be(expectedResult); - } - - [DataTestMethod] - [DataRow("CorrectRuleId", null, null, true)] // exact matches - [DataRow("CorrectRuleId", null, "hash", true)] // hash should be ignored for file-level issues - [DataRow("WrongRuleId", null, null, false)] // wrong rule - [DataRow("CorrectRuleId", 1, "hash", false)] // not a file issue - [DataRow("CorrectRuleId", 999, null, false)] // not a file issue - should not match a file issue, even though the hash is the same - public void IsMatch_FileLevelIssue(string serverRuleId, int? serverIssueLine, - string serverHash, bool expectedResult) - { - // File issues have line number of 0 and an empty hash - var issueToMatch = CreateIssueToMatch("CorrectRuleId", null, null); - var serverIssue = CreateServerIssue(serverRuleId, serverIssueLine, serverHash); - - CreateTestSubject().IsLikelyMatch(issueToMatch, serverIssue).Should().Be(expectedResult); - } - - [DataTestMethod] - [DataRow("CorrectRuleId", null, null, true)] // exact matches - [DataRow("CorrectRuleId", null, "hash", true)] // hash should be ignored for file-level issues - [DataRow("WrongRuleId", null, null, false)] // wrong rule - [DataRow("CorrectRuleId", 20, "hash", true)] // roslyn issue is not actually a file issue, so it should match - [DataRow("CorrectRuleId", 1, null, true)] // roslyn issue is not actually a file issue, so it should match - [DataRow("CorrectRuleId", 999, null, false)] // not a file issue - should not match a file issue, even though the hash is the same - public void IsMatch_PotentialRoslynFileLevelIssue_ServerIssueVariation(string serverRuleId, int? serverIssueLine, - string serverHash, bool expectedResult) - { - // File issues for roslyn have line and column number equal to 1 - var issueToMatch = new FilterableRoslynIssue("CorrectRuleId", null, 1, 1); - issueToMatch.SetLineHash("hash"); // hash is always calculated since we don't know if it's a file issue or not - var serverIssue = CreateServerIssue(serverRuleId, serverIssueLine, serverHash); - - CreateTestSubject().IsLikelyMatch(issueToMatch, serverIssue).Should().Be(expectedResult); - } - - [DataTestMethod] - [DataRow("CorrectRuleId", 1, 1, true)] // potential file issue matches server file issue - [DataRow("CorrectRuleId", 1, 2, false)] // not a file issue - [DataRow("WrongRuleId", 1, 1, false)] // wrong rule - public void IsMatch_PotentialRoslynFileLevelIssue_RoslynIssueVariation(string roslynIssueId, int startLine, int startColumn, bool expectedResult) - { - var issueToMatch = new FilterableRoslynIssue(roslynIssueId, null, startLine, startColumn); - issueToMatch.SetLineHash("hash"); // hash is always calculated since we don't know if it's a file issue or not - var serverIssue = CreateServerIssue("CorrectRuleId", null, null); - - CreateTestSubject().IsLikelyMatch(issueToMatch, serverIssue).Should().Be(expectedResult); - } - - [TestMethod] - // Module-level issues i.e. no file - [DataRow(null, null, true)] - [DataRow(null, "", true)] - [DataRow("", null, true)] - [DataRow("", "", true)] - - // Module-level issues should not match non-module-level issues - [DataRow(@"any.txt", "", false)] - [DataRow(@"any.txt", null, false)] - [DataRow("", @"c:\any.txt", false)] - [DataRow(null, @"c:\any.txt", false)] - - // File issues - [DataRow(@"same.txt", @"c:\same.txt", true)] - [DataRow(@"SAME.TXT", @"c:\same.txt", true)] - [DataRow(@"same.TXT", @"c:\XXXsame.txt", false)] // partial file name -> should not match - [DataRow(@"differentExt.123", @"a:\differentExt.999", false)] // different extension -> should not match - [DataRow(@"aaa\partial\file.cs", @"d:\partial\file.cs", false)] - // Only matching the local path tail, so the same server path can match multiple local files - [DataRow(@"partial\file.cs", @"c:\aaa\partial\file.cs", true)] - [DataRow(@"partial\file.cs", @"c:\aaa\bbb\partial\file.cs", true)] - [DataRow(@"partial\file.cs", @"c:\aaa\bbb\ccc\partial\file.cs", true)] - public void IsMatch_CheckFileComparisons(string serverFilePath, string localFilePath, bool expected) - { - var issueToMatch = CreateIssueToMatch("111", 0, "hash", filePath: localFilePath); - - var serverIssue = CreateServerIssue("111", 0, "hash", filePath: serverFilePath); - - CreateTestSubject().IsLikelyMatch(issueToMatch, serverIssue).Should().Be(expected); - } - - [TestMethod] - public void FindMatchOrDefault_FindsFirstMatch() - { - var ruleId = "111"; - var startLine = 0; - var issueToMatch = CreateIssueToMatch(ruleId, startLine, null, @"c:\root\dir\file.cs"); - var serverPath = @"dir\file.cs"; - - var correctServerIssue = CreateServerIssue(ruleId, startLine, null, serverPath); - - CreateTestSubject().GetFirstLikelyMatchFromSameFileOrNull(issueToMatch, new[] - { - CreateServerIssue("222", startLine, null, serverPath), - CreateServerIssue(ruleId, 111, null, serverPath), - correctServerIssue, - CreateServerIssue(ruleId, startLine, null, serverPath) // finds only the firs match - }).Should().BeSameAs(correctServerIssue); - } - - [TestMethod] - public void FindMatchOrDefault_NoServerIssues_ReturnsNull() - { - var issueToMatch = CreateIssueToMatch("1", 1, "1"); - - CreateTestSubject().GetFirstLikelyMatchFromSameFileOrNull(issueToMatch, Array.Empty()).Should().BeNull(); - } - - private IssueMatcher CreateTestSubject() - { - return new IssueMatcher(); - } - - private IFilterableIssue CreateIssueToMatch(string ruleId, int? startLine, string lineHash, string filePath = null) => - new TestFilterableIssue - { - RuleId = ruleId, - StartLine = startLine, - LineHash = lineHash, - FilePath = filePath - }; - - private SonarQubeIssue CreateServerIssue(string ruleId, int? startLine, string lineHash, - string filePath = null) - { - var sonarQubeIssue = new SonarQubeIssue(null, filePath, lineHash, null, null, ruleId, false, SonarQubeIssueSeverity.Info, - DateTimeOffset.MinValue, DateTimeOffset.MinValue, - startLine.HasValue - ? new IssueTextRange(startLine.Value, 1, 1, 1) - : null, - flows: null); - - return sonarQubeIssue; - } -} diff --git a/src/ConnectedMode.UnitTests/Migration/ConnectedModeMigrationTests.cs b/src/ConnectedMode.UnitTests/Migration/ConnectedModeMigrationTests.cs index 8b8b7ee603..fb5c37080e 100644 --- a/src/ConnectedMode.UnitTests/Migration/ConnectedModeMigrationTests.cs +++ b/src/ConnectedMode.UnitTests/Migration/ConnectedModeMigrationTests.cs @@ -21,7 +21,6 @@ using SonarLint.VisualStudio.ConnectedMode.Binding; using SonarLint.VisualStudio.ConnectedMode.Migration; using SonarLint.VisualStudio.ConnectedMode.Shared; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; using SonarLint.VisualStudio.ConnectedMode.UnitTests.Migration.ConnectedModeMigrationTestsExtensions; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; @@ -50,8 +49,7 @@ public void MefCtor_CheckIsExported() MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), @@ -257,27 +255,26 @@ public async Task Migrate_ThrowsCritical_NotHandled() [TestMethod] public async Task Migrate_CallBindAsync() { - var unintrusiveBindingController = new Mock(); + var configurationPersister = new Mock(); var cancellationToken = CancellationToken.None; - var testSubject = CreateTestSubject(unintrusiveBindingController: unintrusiveBindingController.Object); + var testSubject = CreateTestSubject(configurationPersister: configurationPersister.Object); await testSubject.MigrateAsync(AnyBoundProject, Mock.Of>(), false, cancellationToken); - unintrusiveBindingController.Verify( - x => x.BindAsync( - It.Is(proj => IsExpectedBoundServerProject(proj)), - It.IsAny>(), cancellationToken), Times.Once); + configurationPersister.Verify( + x => x.Persist( + It.Is(proj => IsExpectedBoundServerProject(proj))), Times.Once); } [TestMethod] public async Task Migrate_BoundProjectCanNotBeConvertedToServerConnection_LogsAndDoesB() { var storedConnection = new ServerConnection.SonarQube(AnyBoundProject.ServerUri); - var unintrusiveBindingControllerMock = new Mock(); + var configurationPersisterMock = new Mock(); var serverConnectionsRepositoryMock = CreateServerConnectionsRepositoryMock(); MockIServerConnectionsRepositoryTryGet(serverConnectionsRepositoryMock, storedConnection.Id, storedConnection); var solutionInfoProviderMock = CreateSolutionInfoProviderMock(); - var testSubject = CreateTestSubject(unintrusiveBindingController: unintrusiveBindingControllerMock.Object, + var testSubject = CreateTestSubject(configurationPersister: configurationPersisterMock.Object, serverConnectionsRepository: serverConnectionsRepositoryMock.Object, solutionInfoProvider: solutionInfoProviderMock.Object); await testSubject.MigrateAsync(AnyBoundProject, Mock.Of>(), false, CancellationToken.None); @@ -285,21 +282,20 @@ public async Task Migrate_BoundProjectCanNotBeConvertedToServerConnection_LogsAn serverConnectionsRepositoryMock.Verify(mock => mock.TryGet(storedConnection.Id, out It.Ref.IsAny), Times.Once); serverConnectionsRepositoryMock.Verify(mock => mock.TryAdd(IsExpectedServerConnection(It.IsAny())), Times.Never); solutionInfoProviderMock.Verify(mock => mock.GetSolutionNameAsync(), Times.Once); - unintrusiveBindingControllerMock.Verify( - x => x.BindAsync( - It.Is(proj => IsExpectedBoundServerProject(proj)), - It.IsAny>(), It.IsAny()), Times.Once); + configurationPersisterMock.Verify( + x => x.Persist( + It.Is(proj => IsExpectedBoundServerProject(proj))), Times.Once); } [TestMethod] public async Task Migrate_ConnectionExists_EstablishesBinding() { var storedConnection = new ServerConnection.SonarQube(AnyBoundProject.ServerUri); - var unintrusiveBindingControllerMock = new Mock(); + var configurationPersisterMock = new Mock(); var serverConnectionsRepositoryMock = CreateServerConnectionsRepositoryMock(); MockIServerConnectionsRepositoryTryGet(serverConnectionsRepositoryMock, storedConnection.Id, storedConnection); var solutionInfoProviderMock = CreateSolutionInfoProviderMock(); - var testSubject = CreateTestSubject(unintrusiveBindingController: unintrusiveBindingControllerMock.Object, + var testSubject = CreateTestSubject(configurationPersister: configurationPersisterMock.Object, serverConnectionsRepository: serverConnectionsRepositoryMock.Object, solutionInfoProvider: solutionInfoProviderMock.Object); await testSubject.MigrateAsync(AnyBoundProject, Mock.Of>(), false, CancellationToken.None); @@ -307,20 +303,19 @@ public async Task Migrate_ConnectionExists_EstablishesBinding() serverConnectionsRepositoryMock.Verify(mock => mock.TryGet(storedConnection.Id, out It.Ref.IsAny), Times.Once); serverConnectionsRepositoryMock.Verify(mock => mock.TryAdd(IsExpectedServerConnection(It.IsAny())), Times.Never); solutionInfoProviderMock.Verify(mock => mock.GetSolutionNameAsync(), Times.Once); - unintrusiveBindingControllerMock.Verify( - x => x.BindAsync( - It.Is(proj => IsExpectedBoundServerProject(proj)), - It.IsAny>(), It.IsAny()), Times.Once); + configurationPersisterMock.Verify( + x => x.Persist( + It.Is(proj => IsExpectedBoundServerProject(proj))), Times.Once); } [TestMethod] public async Task Migrate_ConnectionDoesNotExist_AddsConnectionAndEstablishesBinding() { - var unintrusiveBindingControllerMock = new Mock(); + var configurationPersisterMock = new Mock(); var convertedConnection = AnyBoundProject.FromBoundSonarQubeProject(); var serverConnectionsRepositoryMock = CreateServerConnectionsRepositoryMock(); var solutionInfoProviderMock = CreateSolutionInfoProviderMock(); - var testSubject = CreateTestSubject(unintrusiveBindingController: unintrusiveBindingControllerMock.Object, + var testSubject = CreateTestSubject(configurationPersister: configurationPersisterMock.Object, serverConnectionsRepository: serverConnectionsRepositoryMock.Object, solutionInfoProvider: solutionInfoProviderMock.Object); serverConnectionsRepositoryMock.Setup(x => x.TryAdd(IsExpectedServerConnection(convertedConnection))).Returns(true); @@ -329,10 +324,9 @@ public async Task Migrate_ConnectionDoesNotExist_AddsConnectionAndEstablishesBin serverConnectionsRepositoryMock.Verify(mock => mock.TryGet(convertedConnection.Id, out It.Ref.IsAny), Times.Once); serverConnectionsRepositoryMock.Verify(mock => mock.TryAdd(IsExpectedServerConnection(convertedConnection)), Times.Once); solutionInfoProviderMock.Verify(mock => mock.GetSolutionNameAsync(), Times.Once); - unintrusiveBindingControllerMock.Verify( - x => x.BindAsync( - It.Is(proj => IsExpectedBoundServerProject(proj)), - It.IsAny>(), It.IsAny()), Times.Once); + configurationPersisterMock.Verify( + x => x.Persist( + It.Is(proj => IsExpectedBoundServerProject(proj))), Times.Once); } /// @@ -342,21 +336,20 @@ public async Task Migrate_ConnectionDoesNotExist_AddsConnectionAndEstablishesBin [TestMethod] public async Task Migrate_ConnectionDoesNotExist_CannotAdd_DoesNotCreateConnection() { - var unintrusiveBindingController = new Mock(); + var configurationPersister = new Mock(); var convertedConnection = AnyBoundProject.FromBoundSonarQubeProject(); var serverConnectionsRepositoryMock = new Mock(); serverConnectionsRepositoryMock.Setup(x => x.ConnectionsFileExists()).Returns(true); serverConnectionsRepositoryMock.Setup(x => x.TryAdd(convertedConnection)).Returns(false); - var testSubject = CreateTestSubject(unintrusiveBindingController: unintrusiveBindingController.Object, serverConnectionsRepository: serverConnectionsRepositoryMock.Object); + var testSubject = CreateTestSubject(configurationPersister: configurationPersister.Object, serverConnectionsRepository: serverConnectionsRepositoryMock.Object); await testSubject.MigrateAsync(AnyBoundProject, Mock.Of>(), false, CancellationToken.None); serverConnectionsRepositoryMock.Verify(mock => mock.TryGet(convertedConnection.Id, out It.Ref.IsAny), Times.Once); serverConnectionsRepositoryMock.Verify(mock => mock.TryAdd(IsExpectedServerConnection(convertedConnection)), Times.Once); - unintrusiveBindingController.Verify( - x => x.BindAsync( - It.Is(proj => IsExpectedBoundServerProject(proj)), - It.IsAny>(), It.IsAny()), Times.Once); + configurationPersister.Verify( + x => x.Persist( + It.Is(proj => IsExpectedBoundServerProject(proj))), Times.Once); } /// @@ -366,21 +359,20 @@ public async Task Migrate_ConnectionDoesNotExist_CannotAdd_DoesNotCreateConnecti [TestMethod] public async Task Migrate_ConnectionDoesNotExist_Throws_DoesNotCreateConnection() { - var unintrusiveBindingController = new Mock(); + var configurationPersister = new Mock(); var convertedConnection = AnyBoundProject.FromBoundSonarQubeProject(); var serverConnectionsRepositoryMock = new Mock(); serverConnectionsRepositoryMock.Setup(x => x.ConnectionsFileExists()).Returns(true); serverConnectionsRepositoryMock.Setup(x => x.TryAdd(convertedConnection)).Throws(new Exception()); - var testSubject = CreateTestSubject(unintrusiveBindingController: unintrusiveBindingController.Object, serverConnectionsRepository: serverConnectionsRepositoryMock.Object); + var testSubject = CreateTestSubject(configurationPersister: configurationPersister.Object, serverConnectionsRepository: serverConnectionsRepositoryMock.Object); await testSubject.MigrateAsync(AnyBoundProject, Mock.Of>(), false, CancellationToken.None); serverConnectionsRepositoryMock.Verify(mock => mock.TryGet(convertedConnection.Id, out It.Ref.IsAny), Times.Once); serverConnectionsRepositoryMock.Verify(mock => mock.TryAdd(IsExpectedServerConnection(convertedConnection)), Times.Once); - unintrusiveBindingController.Verify( - x => x.BindAsync( - It.Is(proj => IsExpectedBoundServerProject(proj)), - It.IsAny>(), It.IsAny()), Times.Once); + configurationPersister.Verify( + x => x.Persist( + It.Is(proj => IsExpectedBoundServerProject(proj))), Times.Once); } /// @@ -389,13 +381,13 @@ public async Task Migrate_ConnectionDoesNotExist_Throws_DoesNotCreateConnection( [TestMethod] public async Task Migrate_ConnectionsJsonFileDoesNotExistAndNewBindingsExist_DoesNotMigrateServerConnection() { - var unintrusiveBindingControllerMock = new Mock(); + var configurationPersisterMock = new Mock(); var serverConnectionsRepositoryMock = new Mock(); var bindingPathProvider = new Mock(); var logger = new Mock(); var testSubject = CreateTestSubject( serverConnectionsRepository: serverConnectionsRepositoryMock.Object, - unintrusiveBindingController: unintrusiveBindingControllerMock.Object, + configurationPersister: configurationPersisterMock.Object, unintrusiveBindingPathProvider: bindingPathProvider.Object, logger: logger.Object); serverConnectionsRepositoryMock.Setup(mock => mock.ConnectionsFileExists()).Returns(false); @@ -405,10 +397,9 @@ public async Task Migrate_ConnectionsJsonFileDoesNotExistAndNewBindingsExist_Doe logger.Verify(x => x.WriteLine(MigrationStrings.ConnectionsJson_DoesNotExist), Times.Once); serverConnectionsRepositoryMock.Verify(mock => mock.TryAdd(It.IsAny()), Times.Never); - unintrusiveBindingControllerMock.Verify( - x => x.BindAsync( - It.Is(proj => IsExpectedBoundServerProject(proj)), - It.IsAny>(), It.IsAny()), Times.Once); + configurationPersisterMock.Verify( + x => x.Persist( + It.Is(proj => IsExpectedBoundServerProject(proj))), Times.Once); } /// @@ -417,13 +408,13 @@ public async Task Migrate_ConnectionsJsonFileDoesNotExistAndNewBindingsExist_Doe [TestMethod] public async Task Migrate_ConnectionsJsonFileDoesNotExistAndNoNewBindingsExist_MigratesConnection() { - var unintrusiveBindingControllerMock = new Mock(); + var configurationPersisterMock = new Mock(); var serverConnectionsRepositoryMock = new Mock(); var bindingPathProvider = new Mock(); var logger = new Mock(); var testSubject = CreateTestSubject( serverConnectionsRepository: serverConnectionsRepositoryMock.Object, - unintrusiveBindingController: unintrusiveBindingControllerMock.Object, + configurationPersister: configurationPersisterMock.Object, unintrusiveBindingPathProvider: bindingPathProvider.Object, logger: logger.Object); serverConnectionsRepositoryMock.Setup(mock => mock.ConnectionsFileExists()).Returns(false); @@ -433,10 +424,9 @@ public async Task Migrate_ConnectionsJsonFileDoesNotExistAndNoNewBindingsExist_M logger.Verify(x => x.WriteLine(MigrationStrings.ConnectionsJson_DoesNotExist), Times.Never); serverConnectionsRepositoryMock.Verify(mock => mock.TryAdd(It.IsAny()), Times.Once); - unintrusiveBindingControllerMock.Verify( - x => x.BindAsync( - It.Is(proj => IsExpectedBoundServerProject(proj)), - It.IsAny>(), It.IsAny()), Times.Once); + configurationPersisterMock.Verify( + x => x.Persist( + It.Is(proj => IsExpectedBoundServerProject(proj))), Times.Once); } [TestMethod] @@ -449,17 +439,6 @@ public void Migrate_InvalidServerInformation_Throws() act.Should().Throw(); } - [TestMethod] - public async Task Migrate_RoslynSuppressionUpdateIsTriggered() - { - var suppressionsUpdater = new Mock(); - - var testSubject = CreateTestSubject(roslynSuppressionUpdater: suppressionsUpdater.Object); - await testSubject.MigrateAsync(AnyBoundProject, null, false, CancellationToken.None); - - suppressionsUpdater.Verify(x => x.UpdateAllServerSuppressionsAsync(), Times.Once); - } - [TestMethod] public async Task Migrate_SwitchToBackgroundThread() { @@ -479,8 +458,7 @@ private static ConnectedModeMigration CreateTestSubject( IVsAwareFileSystem fileSystem = null, IMigrationSettingsProvider settingsProvider = null, ISonarQubeService sonarQubeService = null, - IUnintrusiveBindingController unintrusiveBindingController = null, - IRoslynSuppressionUpdater roslynSuppressionUpdater = null, + IConfigurationPersister configurationPersister = null, ISharedBindingConfigProvider sharedBindingConfigProvider = null, ILogger logger = null, IThreadHandling threadHandling = null, @@ -492,8 +470,7 @@ private static ConnectedModeMigration CreateTestSubject( fileCleaner ??= Mock.Of(); fileSystem ??= Mock.Of(); sonarQubeService ??= Mock.Of(); - unintrusiveBindingController ??= Mock.Of(); - roslynSuppressionUpdater ??= Mock.Of(); + configurationPersister ??= Mock.Of(); settingsProvider ??= CreateSettingsProvider(DefaultTestLegacySettings).Object; sharedBindingConfigProvider ??= Mock.Of(); solutionInfoProvider ??= CreateSolutionInfoProviderMock().Object; @@ -508,8 +485,7 @@ private static ConnectedModeMigration CreateTestSubject( fileCleaner, fileSystem, sonarQubeService, - unintrusiveBindingController, - roslynSuppressionUpdater, + configurationPersister, sharedBindingConfigProvider, logger, threadHandling, diff --git a/src/ConnectedMode.UnitTests/Persistence/BindingJsonModelConverterTests.cs b/src/ConnectedMode.UnitTests/Persistence/BindingJsonModelConverterTests.cs index 0eab8a2dfa..e80c875296 100644 --- a/src/ConnectedMode.UnitTests/Persistence/BindingJsonModelConverterTests.cs +++ b/src/ConnectedMode.UnitTests/Persistence/BindingJsonModelConverterTests.cs @@ -57,7 +57,6 @@ public void ConvertFromModel_ConvertsCorrectly() var bindingModel = new BindingJsonModel { ProjectKey = "project123", - Profiles = new Dictionary(), ProjectName = "ignored", Organization = new SonarQubeOrganization("ignored", "ignored"), ServerUri = new Uri("http://ignored"), @@ -70,13 +69,12 @@ public void ConvertFromModel_ConvertsCorrectly() boundServerProject.ServerConnection.Should().BeSameAs(connection); boundServerProject.LocalBindingKey.Should().BeSameAs(localBindingKey); boundServerProject.ServerProjectKey.Should().BeSameAs(bindingModel.ProjectKey); - boundServerProject.Profiles.Should().BeSameAs(bindingModel.Profiles); } [TestMethod] public void ConvertToModel_SonarCloudConnection_ConvertsCorrectly() { - var boundServerProject = new BoundServerProject("localBinding", "serverProject", new ServerConnection.SonarCloud("myorg")) { Profiles = new Dictionary() }; + var boundServerProject = new BoundServerProject("localBinding", "serverProject", new ServerConnection.SonarCloud("myorg")); var bindingModel = testSubject.ConvertToModel(boundServerProject); @@ -85,16 +83,12 @@ public void ConvertToModel_SonarCloudConnection_ConvertsCorrectly() bindingModel.ServerUri.Should().BeEquivalentTo(boundServerProject.ServerConnection.ServerUri); bindingModel.Organization.Key.Should().BeSameAs(((ServerConnection.SonarCloud)boundServerProject.ServerConnection).OrganizationKey); bindingModel.ServerConnectionId.Should().BeSameAs(boundServerProject.ServerConnection.Id); - bindingModel.Profiles.Should().BeSameAs(boundServerProject.Profiles); } [TestMethod] public void ConvertToModel_SonarQubeConnection_ConvertsCorrectly() { - var boundServerProject = new BoundServerProject("localBinding", "serverProject", new ServerConnection.SonarQube(new Uri("http://mysq"))) - { - Profiles = new Dictionary() - }; + var boundServerProject = new BoundServerProject("localBinding", "serverProject", new ServerConnection.SonarQube(new Uri("http://mysq"))); var bindingModel = testSubject.ConvertToModel(boundServerProject); @@ -103,7 +97,6 @@ public void ConvertToModel_SonarQubeConnection_ConvertsCorrectly() bindingModel.ServerUri.Should().BeEquivalentTo(boundServerProject.ServerConnection.ServerUri); bindingModel.Organization.Should().BeNull(); bindingModel.ServerConnectionId.Should().BeSameAs(boundServerProject.ServerConnection.Id); - bindingModel.Profiles.Should().BeSameAs(boundServerProject.Profiles); } [TestMethod] @@ -116,7 +109,6 @@ public void ConvertFromModelToLegacy_ConvertsCorrectly() ServerUri = new Uri("http://localhost"), ProjectKey = "project123", ProjectName = "project 123", - Profiles = new Dictionary(), ServerConnectionId = "ignored", }; @@ -126,7 +118,6 @@ public void ConvertFromModelToLegacy_ConvertsCorrectly() legacyBinding.ProjectName.Should().BeSameAs(bindingModel.ProjectName); legacyBinding.ServerUri.Should().BeSameAs(bindingModel.ServerUri); legacyBinding.Organization.Should().BeSameAs(bindingModel.Organization); - legacyBinding.Profiles.Should().BeSameAs(legacyBinding.Profiles); legacyBinding.Credentials.Should().BeSameAs(credentials); } } diff --git a/src/ConnectedMode.UnitTests/Persistence/BindingJsonModelSerializationTests.cs b/src/ConnectedMode.UnitTests/Persistence/BindingJsonModelSerializationTests.cs index f9d9c56ca4..ce4c281c90 100644 --- a/src/ConnectedMode.UnitTests/Persistence/BindingJsonModelSerializationTests.cs +++ b/src/ConnectedMode.UnitTests/Persistence/BindingJsonModelSerializationTests.cs @@ -36,7 +36,7 @@ public class BindingJsonModelSerializationTests "my_project_123", "My Project", /* ignored */ null, - new SonarQubeOrganization("org_key_123", "My Org")) { Profiles = QualityProfiles }; + new SonarQubeOrganization("org_key_123", "My Org")); private readonly BindingJsonModel bindingJsonModel = new() { @@ -45,12 +45,11 @@ public class BindingJsonModelSerializationTests ProjectName = "My Project", Organization = new SonarQubeOrganization("org_key_123", "My Org"), ServerConnectionId = "some_connection_id_123", - Profiles = QualityProfiles }; - private readonly BoundServerProject boundSonarCloudServerProject = new("solution123", "my_project_123", new ServerConnection.SonarCloud("org_key_123")) { Profiles = QualityProfiles }; + private readonly BoundServerProject boundSonarCloudServerProject = new("solution123", "my_project_123", new ServerConnection.SonarCloud("org_key_123")); private readonly BoundServerProject boundSonarQubeServerProject - = new("solution123", "my_project_123", new ServerConnection.SonarQube(new Uri("http://next.sonarqube.com/sonarqube"))) { Profiles = QualityProfiles }; + = new("solution123", "my_project_123", new ServerConnection.SonarQube(new Uri("http://next.sonarqube.com/sonarqube"))); private readonly BindingJsonModelConverter bindingJsonModelConverter = new(); @@ -69,13 +68,7 @@ public void JsonModel_SerializedAsExpected() "Name": "My Org" }, "ProjectKey": "my_project_123", - "ProjectName": "My Project", - "Profiles": { - "C": { - "ProfileKey": "qpkey", - "ProfileTimestamp": "2020-12-31T23:59:59" - } - } + "ProjectName": "My Project" } """); } @@ -94,13 +87,7 @@ public void JsonModel_FromSonarCloudBinding_SerializedAsExpected() "Key": "org_key_123", "Name": null }, - "ProjectKey": "my_project_123", - "Profiles": { - "C": { - "ProfileKey": "qpkey", - "ProfileTimestamp": "2020-12-31T23:59:59" - } - } + "ProjectKey": "my_project_123" } """); } @@ -115,13 +102,7 @@ public void JsonModel_FromSonarQubeBinding_SerializedAsExpected() { "ServerConnectionId": "http://next.sonarqube.com/sonarqube", "ServerUri": "http://next.sonarqube.com/sonarqube", - "ProjectKey": "my_project_123", - "Profiles": { - "C": { - "ProfileKey": "qpkey", - "ProfileTimestamp": "2020-12-31T23:59:59" - } - } + "ProjectKey": "my_project_123" } """); } diff --git a/src/ConnectedMode.UnitTests/Persistence/SolutionBindingFileLoaderTests.cs b/src/ConnectedMode.UnitTests/Persistence/SolutionBindingFileLoaderTests.cs index 2ac8f97e8b..72a2afef54 100644 --- a/src/ConnectedMode.UnitTests/Persistence/SolutionBindingFileLoaderTests.cs +++ b/src/ConnectedMode.UnitTests/Persistence/SolutionBindingFileLoaderTests.cs @@ -55,22 +55,12 @@ public void TestInitialize() ProjectKey = "MyProject Key", ProjectName = "projectName", ServerConnectionId = null, - Profiles = new Dictionary - { - { Language.CSharp, new ApplicableQualityProfile { ProfileKey = "sonar way", ProfileTimestamp = DateTime.Parse("2020-02-25T08:57:54+0000") } } - } }; serializedProject = @"{ ""ServerUri"": ""http://xxx.www.zzz/yyy:9000"", ""ProjectKey"": ""MyProject Key"", - ""ProjectName"": ""projectName"", - ""Profiles"": { - ""CSharp"": { - ""ProfileKey"": ""sonar way"", - ""ProfileTimestamp"": ""2020-02-25T08:57:54Z"" - } - } + ""ProjectName"": ""projectName"" }"; } @@ -203,23 +193,6 @@ public void Load_FileExists_DeserializedProject() actual.Should().BeEquivalentTo(bindingJsonModel); } - [TestMethod] - public void Load_FileExists_ProjectWithNonUtcTimestamp_DeserializedProjectWithCorrectTimestampData() - { - const string utcDate = "2020-02-25T08:57:54Z"; - const string localDate = "2020-02-25T10:57:54+02:00"; - serializedProject = serializedProject.Replace(utcDate, localDate); - - MockFileExists(MockFilePath); - fileSystem.File.ReadAllText(MockFilePath).Returns(serializedProject); - - var actual = testSubject.Load(MockFilePath); - actual.Should().BeEquivalentTo(bindingJsonModel); - - var deserializedTimestamp = actual.Profiles[Language.CSharp].ProfileTimestamp.Value.ToUniversalTime(); - deserializedTimestamp.Should().Be(new DateTime(2020, 2, 25, 8, 57, 54)); - } - [TestMethod] public void DeleteBindingDirectory_ConfigFilePathNotExists_ReturnsFalseAndLogs() { diff --git a/src/ConnectedMode.UnitTests/ProjectRootCalculatorTests.cs b/src/ConnectedMode.UnitTests/ProjectRootCalculatorTests.cs deleted file mode 100644 index 649d00c813..0000000000 --- a/src/ConnectedMode.UnitTests/ProjectRootCalculatorTests.cs +++ /dev/null @@ -1,92 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Threading; -using System.Threading.Tasks; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests; - -[TestClass] -public class ProjectRootCalculatorTests -{ - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void MefCtor_CheckIsSingleton() - { - MefTestHelpers.CheckIsSingletonMefComponent(); - } - - [TestMethod] - public async Task CalculateBasedOnLocalPathAsync_StandaloneMode_ReturnsNull() - { - var testSubject = CreateTestSubject(out _, out var configurationProviderMock, out _); - configurationProviderMock.SetupGet(x => x.CurrentConfiguration).Returns(BindingConfiguration.Standalone); - - var result = await testSubject.CalculateBasedOnLocalPathAsync(@"c:\somepath", CancellationToken.None); - - result.Should().BeNull(); - } - - [TestMethod] - public async Task CalculateBasedOnLocalPathAsync_ConnectedMode_ReturnsCorrectRoot() - { - const string projectKey = "projectKey"; - const string branch = "branch"; - - var testSubject = CreateTestSubject(out var sonarQubeServiceMock, out var configurationProviderMock, out var branchProviderMock); - configurationProviderMock - .SetupGet(x => x.CurrentConfiguration) - .Returns(BindingConfiguration.CreateBoundConfiguration( - new BoundServerProject("solution", projectKey, new ServerConnection.SonarQube(new Uri("http://localhost"))), - SonarLintMode.Connected, - "somedir")); - branchProviderMock - .Setup(x => x.GetServerBranchNameAsync(It.IsAny())) - .ReturnsAsync(branch); - sonarQubeServiceMock - .Setup(x => x.SearchFilesByNameAsync(projectKey, branch, "file.cs", CancellationToken.None)) - .ReturnsAsync(new []{@"dir\file.cs"}); - - var result = await testSubject.CalculateBasedOnLocalPathAsync(@"c:\root\dir\file.cs", CancellationToken.None); - - result.Should().Be(@"c:\root\"); // more extensive testing of the marching algorithm is done in PathHelper tests - } - - private ProjectRootCalculator CreateTestSubject(out Mock sonarQubeServiceMock, - out Mock activeSolutionBoundTracker, - out Mock statefulServerBranchProviderMock) - { - return new ProjectRootCalculator((sonarQubeServiceMock = new Mock(MockBehavior.Strict)).Object, - (activeSolutionBoundTracker = new Mock(MockBehavior.Strict)).Object, - (statefulServerBranchProviderMock = new Mock(MockBehavior.Strict)).Object); - } -} diff --git a/src/ConnectedMode.UnitTests/QualityProfiles/OutOfDateQualityProfileFinderTests.cs b/src/ConnectedMode.UnitTests/QualityProfiles/OutOfDateQualityProfileFinderTests.cs deleted file mode 100644 index 32e0eef308..0000000000 --- a/src/ConnectedMode.UnitTests/QualityProfiles/OutOfDateQualityProfileFinderTests.cs +++ /dev/null @@ -1,233 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.QualityProfiles; - -[TestClass] -public class OutOfDateQualityProfileFinderTests -{ - private static readonly Uri AnyUri = new("http://localhost"); - private const string Project = "project"; - private const string Organization = "organization"; - - [TestMethod] - public void MefCtor_CheckExports() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void Mef_CheckIsSingleton() - { - MefTestHelpers.CheckIsSingletonMefComponent(); - } - - [TestMethod] - public async Task GetAsync_SkipsUnknownLanguage() - { - var testSubject = CreateTestSubject(out var sonarQubeServiceMock, Project, Organization, - new SonarQubeQualityProfile("key", "name", "unsupportedlanguage", false, DateTime.UtcNow)); - - var profiles = await testSubject.GetAsync(CreateArgument(Project, Organization, null), CancellationToken.None); - - profiles.Should().BeEmpty(); - sonarQubeServiceMock.Verify(x => x.GetAllQualityProfilesAsync(Project, Organization, CancellationToken.None), Times.Once); - } - - [TestMethod] - public async Task GetAsync_SkipsUpToDateQualityProfile() - { - const string qpKey = "key"; - var timestamp = DateTime.UtcNow; - - var testSubject = CreateTestSubject(out var sonarQubeServiceMock, Project, Organization, - new SonarQubeQualityProfile(qpKey, "name", Language.Ts.ServerLanguageKey, false, timestamp)); - - var profiles = await testSubject.GetAsync( - CreateArgument(Project, - Organization, - new Dictionary { { Language.Ts, new ApplicableQualityProfile { ProfileKey = qpKey, ProfileTimestamp = timestamp } } }), - CancellationToken.None); - - profiles.Should().BeEmpty(); - sonarQubeServiceMock.Verify(x => x.GetAllQualityProfilesAsync(Project, Organization, CancellationToken.None), Times.Once); - } - - [TestMethod] - public async Task GetAsync_WhenBoundProjectDoesNotContainLanguage_SkipsUpToDateQualityProfile() - { - const string qpKey = "key"; - var timestamp = DateTime.UtcNow; - - var testSubject = CreateTestSubject(out var sonarQubeServiceMock, Project, Organization, - new SonarQubeQualityProfile(qpKey, "name", Language.Ts.ServerLanguageKey, false, timestamp)); - - var profiles = await testSubject.GetAsync( - CreateArgument(Project, - Organization, - new Dictionary()), - CancellationToken.None); - - profiles.Should().BeEmpty(); - sonarQubeServiceMock.Verify(x => x.GetAllQualityProfilesAsync(Project, Organization, CancellationToken.None), Times.Once); - } - - [TestMethod] - public async Task GetAsync_DifferentKey_ReturnsQP() - { - const string serverQpKey = "key1"; - const string localQpKey = "key2"; - var timestampServer = DateTime.UtcNow; - var timestampLocal = timestampServer.AddHours(1); // local timestamp is greater, but the key is more important - var sonarQubeQualityProfile = new SonarQubeQualityProfile(serverQpKey, "name", Language.Ts.ServerLanguageKey, false, timestampServer); - - var testSubject = CreateTestSubject(out var sonarQubeServiceMock, Project, Organization, - sonarQubeQualityProfile); - - var profiles = await testSubject.GetAsync( - CreateArgument(Project, - Organization, - new Dictionary { { Language.Ts, new ApplicableQualityProfile { ProfileKey = localQpKey, ProfileTimestamp = timestampLocal } } }), - CancellationToken.None); - - profiles.Should().BeEquivalentTo((Language.Ts, sonarQubeQualityProfile)); - sonarQubeServiceMock.Verify(x => x.GetAllQualityProfilesAsync(Project, Organization, CancellationToken.None), Times.Once); - } - - [TestMethod] - public async Task GetAsync_OutdatedLocalTimestamp_ReturnsQP() - { - const string qpKey = "key"; - var localTimestamp = DateTime.UtcNow; - var serverTimestamp = localTimestamp.AddHours(1); - var sonarQubeQualityProfile = new SonarQubeQualityProfile(qpKey, "name", Language.Ts.ServerLanguageKey, false, serverTimestamp); - - var testSubject = CreateTestSubject(out var sonarQubeServiceMock, Project, Organization, - sonarQubeQualityProfile); - - var profiles = await testSubject.GetAsync( - CreateArgument(Project, - Organization, - new Dictionary { { Language.Ts, new ApplicableQualityProfile { ProfileKey = qpKey, ProfileTimestamp = localTimestamp } } }), - CancellationToken.None); - - profiles.Should().BeEquivalentTo((Language.Ts, sonarQubeQualityProfile)); - sonarQubeServiceMock.Verify(x => x.GetAllQualityProfilesAsync(Project, Organization, CancellationToken.None), Times.Once); - } - - [TestMethod] - public async Task GetAsync_NullLocalQP_ReturnsQP() - { - const string qpKey = "key"; - var serverTimestamp = DateTime.UtcNow; - var sonarQubeQualityProfile = new SonarQubeQualityProfile(qpKey, "name", Language.Ts.ServerLanguageKey, false, serverTimestamp); - - var testSubject = CreateTestSubject(out var sonarQubeServiceMock, Project, Organization, - sonarQubeQualityProfile); - - var profiles = await testSubject.GetAsync( - CreateArgument(Project, - Organization, - new Dictionary { { Language.Ts, new ApplicableQualityProfile { ProfileKey = null, ProfileTimestamp = DateTime.MinValue } } }), - CancellationToken.None); - - profiles.Should().BeEquivalentTo((Language.Ts, sonarQubeQualityProfile)); - sonarQubeServiceMock.Verify(x => x.GetAllQualityProfilesAsync(Project, Organization, CancellationToken.None), Times.Once); - } - - [TestMethod] - public async Task GetAsync_NullOrganization_DoesNotThrow() - { - var testSubject = CreateTestSubject(out var sonarQubeServiceMock, Project, null); - - var act = () => testSubject.GetAsync( - CreateArgument(Project, - null, - new Dictionary()), - CancellationToken.None); - - await act.Should().NotThrowAsync(); - sonarQubeServiceMock.Verify( - x => - x.GetAllQualityProfilesAsync(Project, null, CancellationToken.None), - Times.Once); - } - - [TestMethod] - public async Task GetAsync_MultipleQualityProfiles_ReturnsQP() - { - var serverKey = "key"; - var localKey = "localKey"; - var timestamp = DateTime.UtcNow; - var csharpQp = new SonarQubeQualityProfile(serverKey, "name", Language.CSharp.ServerLanguageKey, false, timestamp); - var cssQp = new SonarQubeQualityProfile(serverKey, "name", Language.Css.ServerLanguageKey, false, timestamp); - var jsQp = new SonarQubeQualityProfile(serverKey, "name", Language.Js.ServerLanguageKey, false, timestamp); - - var testSubject = CreateTestSubject(out var sonarQubeServiceMock, Project, Organization, - csharpQp, - cssQp, - jsQp); - - var profiles = await testSubject.GetAsync( - CreateArgument(Project, - Organization, - new Dictionary - { - { Language.Js, new ApplicableQualityProfile { ProfileKey = localKey, ProfileTimestamp = timestamp } }, - { Language.CSharp, new ApplicableQualityProfile { ProfileKey = localKey, ProfileTimestamp = timestamp } }, - { Language.Css, new ApplicableQualityProfile { ProfileKey = serverKey, ProfileTimestamp = timestamp } } // same qp - }), - CancellationToken.None); - - profiles.Should().BeEquivalentTo((Language.CSharp, csharpQp), (Language.Js, jsQp)); - sonarQubeServiceMock.Verify(x => x.GetAllQualityProfilesAsync(Project, Organization, CancellationToken.None), Times.Once); - } - - private static BoundServerProject CreateArgument( - string project, - string organization, - Dictionary profiles) => - new("solution", - project, - organization == null ? new ServerConnection.SonarQube(AnyUri) : new ServerConnection.SonarCloud(organization)) { Profiles = profiles }; - - private IOutOfDateQualityProfileFinder CreateTestSubject( - out Mock sonarQubeServiceMock, - string project, - string organization, - params SonarQubeQualityProfile[] qualityProfiles) - { - sonarQubeServiceMock = new Mock(); - sonarQubeServiceMock - .Setup(x => x.GetAllQualityProfilesAsync(project, organization, It.IsAny())) - .Returns(Task.FromResult>(qualityProfiles)); - - return new OutOfDateQualityProfileFinder(sonarQubeServiceMock.Object, LanguageProvider.Instance); - } -} diff --git a/src/ConnectedMode.UnitTests/QualityProfiles/QualityProfileUpdaterTests.cs b/src/ConnectedMode.UnitTests/QualityProfiles/QualityProfileUpdaterTests.cs deleted file mode 100644 index 87d0940e96..0000000000 --- a/src/ConnectedMode.UnitTests/QualityProfiles/QualityProfileUpdaterTests.cs +++ /dev/null @@ -1,218 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.Binding; -using SonarLint.VisualStudio.ConnectedMode.Helpers; -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.TestInfrastructure; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.QualityProfiles; - -[TestClass] -public class QualityProfileUpdaterTests -{ - [TestMethod] - public void MefCtor_CheckIsExported() - => MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - - [TestMethod] - public void MefCtor_CheckIsSingleton() - => MefTestHelpers.CheckIsSingletonMefComponent(); - - [TestMethod] - [DataRow(SonarLintMode.Standalone)] - [DataRow(SonarLintMode.LegacyConnected)] - public async Task UpdateBoundSolutionAsync_NotNewConnectedMode_DoesNotUpdateQP(SonarLintMode mode) - { - var configProvider = CreateConfigProvider(mode, CreateDefaultProject()); - var qpDownloader = new Mock(); - var runner = CreatePassthroughRunner(); - - var testSubject = CreateTestSubject(configProvider.Object, qpDownloader.Object, runner.Object); - - await testSubject.UpdateAsync(); - - configProvider.Verify(x => x.GetConfiguration(), Times.Once); - runner.Invocations.Should().BeEmpty(); - qpDownloader.Invocations.Should().BeEmpty(); - } - - [TestMethod] - public async Task UpdateBoundSolutionAsync_IsNewConnectedMode_UpdateIsDoneThroughRunner() - { - var cancellationToken = CancellationToken.None; - var boundProject = CreateDefaultProject(); - var configProvider = CreateConfigProvider(SonarLintMode.Connected, boundProject); - var qpDownloader = new Mock(); - SetUpDownloader(qpDownloader); - // Using a pass-through runner so we can test the action passed to the runner - var runner = CreatePassthroughRunner(cancellationToken); - - var testSubject = CreateTestSubject(configProvider.Object, qpDownloader.Object, runner.Object); - - await testSubject.UpdateAsync(); - - configProvider.Verify(x => x.GetConfiguration(), Times.Once); - runner.Verify(x => x.RunAsync(It.IsAny>()), Times.Once); - qpDownloader.Verify(x => x.UpdateAsync(boundProject, null, cancellationToken), Times.Once); - } - - [TestMethod] - public async Task UpdateBoundSolutionAsync_IsNewConnectedMode_NoUpdates_EventIsNotRaised() - { - var boundProject = CreateDefaultProject(); - var configProvider = CreateConfigProvider(SonarLintMode.Connected, boundProject); - var qpDownloader = new Mock(); - SetUpDownloader(qpDownloader, false); - // Using a pass-through runner so we can test the action passed to the runner - var runner = CreatePassthroughRunner(); - - var testSubject = CreateTestSubject(configProvider.Object, qpDownloader.Object, runner.Object); - - await testSubject.UpdateAsync(); - - configProvider.Verify(x => x.GetConfiguration(), Times.Once); - runner.Verify(x => x.RunAsync(It.IsAny>()), Times.Once); - qpDownloader.Verify(x => x.UpdateAsync(boundProject, null, It.IsAny()), Times.Once); - } - - [TestMethod] - public async Task UpdateBoundSolutionAsync_IsNewConnectedMode_UpdaterDoesNotCallDownloaderDirectly() - { - var configProvider = CreateConfigProvider(SonarLintMode.Connected, CreateDefaultProject()); - var qpDownloader = new Mock(); - // Here, we're not using a pass-through runner, so we're not expecting the - // downloader to be invoked - var runner = new Mock(); - - var testSubject = CreateTestSubject(configProvider.Object, qpDownloader.Object, runner.Object); - - await testSubject.UpdateAsync(); - - configProvider.Verify(x => x.GetConfiguration(), Times.Once); - runner.Verify(x => x.RunAsync(It.IsAny>()), Times.Once); - - // The updater should not be calling the downloader directly, only via the runner - qpDownloader.Invocations.Should().BeEmpty(); - } - - [TestMethod] - public void Dispose_RunnerIsDisposed() - { - var runner = new Mock(); - var testSubject = CreateTestSubject(runner: runner.Object); - runner.Invocations.Should().BeEmpty(); - - testSubject.Dispose(); - - runner.Verify(x => x.Dispose(), Times.Once); - } - - [TestMethod] - public async Task UpdateBoundSolutionAsync_JobIsCancelled_EventIsNotRaised() - { - // Simulate the runner cancelling a task - var cts = new CancellationTokenSource(); - cts.Cancel(); - var runner = new Mock(); - runner.Setup(x => x.RunAsync(It.IsAny>())) - .Callback>(_ => cts.Token.ThrowIfCancellationRequested()); - - var configProvider = CreateConfigProvider(SonarLintMode.Connected, CreateDefaultProject()); - var qpDownloader = new Mock(); - - var testSubject = CreateTestSubject(configProvider.Object, qpDownloader.Object, runner.Object); - - await testSubject.UpdateAsync(); - - configProvider.Verify(x => x.GetConfiguration(), Times.Once); - runner.Verify(x => x.RunAsync(It.IsAny>()), Times.Once); - cts.Dispose(); - } - - [TestMethod] - public async Task UpdateBoundSolutionAsync_InvalidOperationException_EventIsNotRaised() - { - var runner = new Mock(); - runner - .Setup(x => - x.RunAsync(It.IsAny>())) - .Throws(new InvalidOperationException()); - - var configProvider = CreateConfigProvider(SonarLintMode.Connected, CreateDefaultProject()); - - var testSubject = CreateTestSubject(configProvider.Object, runner: runner.Object); - - await testSubject.UpdateAsync(); - - configProvider.Verify(x => x.GetConfiguration(), Times.Once); - runner.Verify(x => x.RunAsync(It.IsAny>()), Times.Once); - } - - private static void SetUpDownloader(Mock qpDownloader, bool result = true) - { - qpDownloader - .Setup(x => - x.UpdateAsync(It.IsAny(), - It.IsAny>(), - It.IsAny())) - .ReturnsAsync(result); - } - - private BoundServerProject CreateDefaultProject() => new("solution", "project", new ServerConnection.SonarCloud("org")); - - private Mock CreateConfigProvider(SonarLintMode mode, BoundServerProject boundProject) - { - var config = new BindingConfiguration(boundProject, mode, "any directory"); - - var configProvider = new Mock(); - configProvider.Setup(x => x.GetConfiguration()).Returns(config); - - return configProvider; - } - - private static Mock CreatePassthroughRunner(CancellationToken? token = null) - { - // If we want to test any of the code in the action passed to the runner we need - // to configure the runner to call it - var runner = new Mock(); - runner - .Setup(x => x.RunAsync(It.IsAny>())) - .Callback((Func callback) => callback(token ?? CancellationToken.None)); - - return runner; - } - - private static QualityProfileUpdater CreateTestSubject( - IConfigurationProvider configProvider = null, - IQualityProfileDownloader qpDownloader = null, - ICancellableActionRunner runner = null) - => new( - configProvider, - qpDownloader, - runner ?? CreatePassthroughRunner().Object, - new TestLogger(logToConsole: true)); -} diff --git a/src/ConnectedMode.UnitTests/QualityProfiles/RoslynQualityProfileDownloaderTests.cs b/src/ConnectedMode.UnitTests/QualityProfiles/RoslynQualityProfileDownloaderTests.cs deleted file mode 100644 index ea3ba021d8..0000000000 --- a/src/ConnectedMode.UnitTests/QualityProfiles/RoslynQualityProfileDownloaderTests.cs +++ /dev/null @@ -1,381 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.Binding; -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.QualityProfiles; - -[TestClass] -public class RoslynQualityProfileDownloaderTests -{ - [TestMethod] - public void MefCtor_CheckIsExported() => - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - - [TestMethod] - public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); - - [TestMethod] - public async Task UpdateAsync_NothingToUpdate_ReturnsFalse() - { - var boundSonarQubeProject = CreateBoundProject(); - SetupLanguagesToUpdate(out var outOfDateQualityProfileFinderMock, - boundSonarQubeProject, - Array.Empty()); - var logger = new TestLogger(); - - var bindingConfigProvider = new Mock(); - - var testSubject = CreateTestSubject(outOfDateQualityProfileFinderMock.Object, - bindingConfigProvider.Object, - logger: logger); - - var result = await testSubject.UpdateAsync(boundSonarQubeProject, null, CancellationToken.None); - - result.Should().BeFalse(); - bindingConfigProvider.Invocations.Should().BeEmpty(); - - logger.AssertPartialOutputStringExists(string.Format(QualityProfilesStrings.SubTextPaddingFormat, QualityProfilesStrings.DownloadingQualityProfilesNotNeeded)); - } - - [TestMethod] - public async Task UpdateAsync_MultipleQPs_ProgressEventsAreRaised() - { - // Arrange - var logger = new TestLogger(logToConsole: true); - var boundProject = CreateBoundProject(); - - var languagesToBind = new[] { Language.CSharp, Language.VBNET }; - - SetupLanguagesToUpdate(out var outOfDateQualityProfileFinderMock, - boundProject, - languagesToBind); - - var bindingConfigProviderMock = new Mock(); - SetupConfigSave(bindingConfigProviderMock, Language.CSharp); - SetupConfigSave(bindingConfigProviderMock, Language.VBNET); - - var testSubject = CreateTestSubject( - languageProvider: CreateLanguageProvider(languagesToBind).Object, - outOfDateQualityProfileFinder: outOfDateQualityProfileFinderMock.Object, - bindingConfigProvider: bindingConfigProviderMock.Object, - logger: logger); - - var notifications = new List(); - var progressAdapter = new Mock>(); - progressAdapter.Setup(x => x.Report(It.IsAny())) - .Callback(x => - (notifications).Add(x)); - - // Act - var result = await testSubject.UpdateAsync(boundProject, progressAdapter.Object, CancellationToken.None); - - // Assert - result.Should().BeTrue(); - - // Progress notifications - percentage complete and messages - notifications.Should().BeEquivalentTo(new[] - { - new FixedStepsProgress(string.Format(QualityProfilesStrings.DownloadingQualityProfileProgressMessage, Language.CSharp.Name), 1, 2), - new FixedStepsProgress(string.Format(QualityProfilesStrings.DownloadingQualityProfileProgressMessage, Language.VBNET.Name), 2, 2), - }); - } - - [TestMethod] - public async Task UpdateAsync_OnNewBoundProject_InitializesOnlyRoslynLanguages() - { - // Arrange - var boundProject = CreateBoundProject(); - var logger = new TestLogger(); - var progressAdapter = new Mock>(); - var mockLanguageProvider = CreateLanguageProvider(); - - var outOfDateQualityProfileFinder = new Mock(); - outOfDateQualityProfileFinder - .Setup(x => x.GetAsync(boundProject, It.IsAny())) - .ReturnsAsync([]); - - boundProject.Profiles.Should().BeNull(); - - var testSubject = CreateTestSubject( - bindingConfigProvider: new Mock().Object, - configurationPersister: new DummyConfigPersister(), - outOfDateQualityProfileFinder: outOfDateQualityProfileFinder.Object, - languageProvider: mockLanguageProvider.Object, - logger: logger); - - // Act - await testSubject.UpdateAsync(boundProject, progressAdapter.Object, CancellationToken.None); - - // Assert - boundProject.Profiles.Count.Should().Be(2); - boundProject.Profiles.ContainsKey(Language.VBNET).Should().BeTrue(); - boundProject.Profiles.ContainsKey(Language.CSharp).Should().BeTrue(); - mockLanguageProvider.Verify(x => x.RoslynLanguages, Times.Once); - mockLanguageProvider.VerifyNoOtherCalls(); - } - - [TestMethod] - public async Task UpdateAsync_UpdatesOnlyRoslynLanguages() - { - // Arrange - var boundProject = CreateBoundProject(); - var logger = new TestLogger(logToConsole: true); - - var mockLanguageProvider = CreateLanguageProvider(); - - // Configure available languages on the server - SetupLanguagesToUpdate(out var outOfDateQualityProfileFinderMock, - boundProject, - Language.CSharp, Language.VBNET, Language.Cpp); - - var configProvider = new Mock(MockBehavior.Strict); - SetupConfigSave(configProvider, Language.CSharp); - SetupConfigSave(configProvider, Language.VBNET); - SetupConfigSave(configProvider, Language.Cpp); - - var configPersister = new DummyConfigPersister(); - - var testSubject = CreateTestSubject( - bindingConfigProvider: configProvider.Object, - configurationPersister: configPersister, - outOfDateQualityProfileFinder: outOfDateQualityProfileFinderMock.Object, - languageProvider: mockLanguageProvider.Object, - logger: logger); - - // Act - var result = await testSubject.UpdateAsync(boundProject, Mock.Of>(), CancellationToken.None); - - // Assert - result.Should().BeTrue(); - - CheckRuleConfigSaved(configProvider, Language.CSharp); - CheckRuleConfigSaved(configProvider, Language.VBNET); - CheckRuleConfigNotSaved(configProvider, Language.Cpp); - - boundProject.Profiles.Count.Should().Be(2); - boundProject.Profiles[Language.VBNET].ProfileKey.Should().NotBeNull(); - boundProject.Profiles[Language.CSharp].ProfileKey.Should().NotBeNull(); - mockLanguageProvider.Verify(x => x.RoslynLanguages, Times.Once); - mockLanguageProvider.VerifyNoOtherCalls(); - } - - [TestMethod] - public async Task UpdateAsync_WhenQualityProfileIsNotAvailable_OtherLanguagesDownloadedSuccessfully() - { - var boundProject = CreateBoundProject(); - // Arrange - var logger = new TestLogger(logToConsole: true); - var languagesToBind = new[] - { - Language.Cpp, // unavailable - Language.CSharp, - Language.Secrets, // unavailable - Language.VBNET - }; - - // Configure available languages on the server - SetupLanguagesToUpdate(out var outOfDateQualityProfileFinderMock, - boundProject, - Language.CSharp, - Language.VBNET); - - var configProvider = new Mock(MockBehavior.Strict); - SetupConfigSave(configProvider, Language.Cpp); - SetupConfigSave(configProvider, Language.CSharp); - SetupConfigSave(configProvider, Language.Secrets); - SetupConfigSave(configProvider, Language.VBNET); - - var configPersister = new DummyConfigPersister(); - - var testSubject = CreateTestSubject( - bindingConfigProvider: configProvider.Object, - configurationPersister: configPersister, - languageProvider: CreateLanguageProvider(languagesToBind).Object, - outOfDateQualityProfileFinder: outOfDateQualityProfileFinderMock.Object, - logger: logger); - - // Act - var result = await testSubject.UpdateAsync(boundProject, Mock.Of>(), CancellationToken.None); - - // Assert - result.Should().BeTrue(); - - CheckRuleConfigSaved(configProvider, Language.CSharp); - CheckRuleConfigSaved(configProvider, Language.VBNET); - CheckRuleConfigNotSaved(configProvider, Language.Cpp); - CheckRuleConfigNotSaved(configProvider, Language.Secrets); - - boundProject.Profiles.Count.Should().Be(4); - boundProject.Profiles[Language.VBNET].ProfileKey.Should().NotBeNull(); - boundProject.Profiles[Language.CSharp].ProfileKey.Should().NotBeNull(); - boundProject.Profiles[Language.Cpp].ProfileKey.Should().BeNull(); - boundProject.Profiles[Language.Secrets].ProfileKey.Should().BeNull(); - } - - [TestMethod] - public async Task UpdateAsync_SavesConfiguration() - { - // Arrange - var boundProject = CreateBoundProject(); - var configPersister = new DummyConfigPersister(); - - const string myProfileKey = "my profile key"; - var language = Language.VBNET; - var serverQpTimestamp = DateTime.UtcNow.AddHours(-2); - var qp = CreateQualityProfile(myProfileKey, serverQpTimestamp); - - SetupLanguagesToUpdate(out var outOfDateQualityProfileFinderMock, - boundProject, - (language, qp)); - - var configProviderMock = new Mock(); - configProviderMock.Setup(x => x.SaveConfigurationAsync(qp, - language, - It.IsAny(), CancellationToken.None)) - .Returns(Task.CompletedTask); - - var testSubject = CreateTestSubject( - outOfDateQualityProfileFinderMock.Object, - configProviderMock.Object, - configurationPersister: configPersister, - languageProvider: CreateLanguageProvider([language]).Object); - - // Act - await testSubject.UpdateAsync(boundProject, null, CancellationToken.None); - - // Assert - configPersister.SavedProject.Should().NotBeNull(); - - var savedProject = configPersister.SavedProject; - savedProject.ServerConnection.Id.Should().Be(boundProject.ServerConnection.Id); - savedProject.Profiles.Should().HaveCount(1); - savedProject.Profiles[Language.VBNET].ProfileKey.Should().Be(myProfileKey); - savedProject.Profiles[Language.VBNET].ProfileTimestamp.Should().Be(serverQpTimestamp); - } - - #region Helpers - - private static RoslynQualityProfileDownloader CreateTestSubject( - IOutOfDateQualityProfileFinder outOfDateQualityProfileFinder = null, - IBindingConfigProvider bindingConfigProvider = null, - DummyConfigPersister configurationPersister = null, - ILogger logger = null, - ILanguageProvider languageProvider = null) => - new( - bindingConfigProvider ?? Mock.Of(), - configurationPersister ?? new DummyConfigPersister(), - outOfDateQualityProfileFinder ?? Mock.Of(), - logger ?? new TestLogger(logToConsole: true), - languageProvider ?? CreateLanguageProvider().Object); - - private static SonarQubeQualityProfile CreateQualityProfile(string key = "key", DateTime timestamp = default) => new(key, default, default, default, timestamp); - - private static void SetupLanguagesToUpdate( - out Mock outOfDateQualityProfileFinderMock, - BoundServerProject boundProject, - params Language[] languages) => - SetupLanguagesToUpdate(out outOfDateQualityProfileFinderMock, - boundProject, - languages.Select(x => (x, CreateQualityProfile())).ToArray()); - - private static void SetupLanguagesToUpdate( - out Mock outOfDateQualityProfileFinderMock, - BoundServerProject boundProject, - params (Language language, SonarQubeQualityProfile qualityProfile)[] qps) - { - outOfDateQualityProfileFinderMock = new Mock(); - outOfDateQualityProfileFinderMock - .Setup(x => - x.GetAsync(boundProject, It.IsAny())) - .ReturnsAsync(qps); - } - - private static void SetupConfigSave( - Mock bindingConfigProvider, - Language language) - { - bindingConfigProvider.Setup(x => x.SaveConfigurationAsync( - It.IsAny(), - language, - It.IsAny(), - CancellationToken.None)) - .Returns(Task.CompletedTask); - } - - private static BoundServerProject CreateBoundProject( - string projectKey = "key", - Uri uri = null) => - new BoundServerProject( - "solution", - projectKey, - new ServerConnection.SonarQube(uri ?? new Uri("http://localhost/"))); - - private static void CheckRuleConfigSaved(Mock bindingConfig, Language language) => - bindingConfig.Verify( - x => - x.SaveConfigurationAsync( - It.IsAny(), - language, - It.IsAny(), - It.IsAny()), - Times.Once); - - private static void CheckRuleConfigNotSaved(Mock bindingConfig, Language language) => - bindingConfig.Verify( - x => - x.SaveConfigurationAsync( - It.IsAny(), - language, - It.IsAny(), - It.IsAny()), - Times.Never); - - private class DummyConfigPersister : IConfigurationPersister - { - public BoundServerProject SavedProject { get; private set; } - - BindingConfiguration IConfigurationPersister.Persist(BoundServerProject project) - { - SavedProject = project; - return new BindingConfiguration(project, SonarLintMode.Connected, "c:\\any"); - } - } - - private static Mock CreateLanguageProvider(Language[] languagesToBind = null) - { - var mockLanguageProvider = new Mock(); - mockLanguageProvider.Setup(x => x.RoslynLanguages) - .Returns(languagesToBind ?? LanguageProvider.Instance.RoslynLanguages.ToArray()); - return mockLanguageProvider; - } - - #endregion Helpers -} diff --git a/src/ConnectedMode.UnitTests/ServerBranchProviderTests.cs b/src/ConnectedMode.UnitTests/ServerBranchProviderTests.cs index 568615b17f..541672b4f3 100644 --- a/src/ConnectedMode.UnitTests/ServerBranchProviderTests.cs +++ b/src/ConnectedMode.UnitTests/ServerBranchProviderTests.cs @@ -18,221 +18,135 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Threading; -using System.Threading.Tasks; using LibGit2Sharp; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; +using SonarLint.VisualStudio.SLCore.Listener.Branch; using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client; -using SonarQube.Client.Models; namespace SonarLint.VisualStudio.ConnectedMode.UnitTests { [TestClass] public class ServerBranchProviderTests { - [TestMethod] - public void MefCtor_CheckIsExported() + private IActiveSolutionBoundTracker activeSolutionBoundTracker; + private IGitWorkspaceService gitWorkspaceService; + private IBranchMatcher branchMatcher; + private TestLogger logger; + private ServerBranchProvider.CreateRepositoryObject createRepoOp; + private ServerBranchProvider testSubject; + private IRepository repo; + + [TestInitialize] + public void TestInitialize() { + activeSolutionBoundTracker = Substitute.For(); + gitWorkspaceService = Substitute.For(); + branchMatcher = Substitute.For(); + logger = Substitute.ForPartsOf(); + createRepoOp = Substitute.For(); + repo = Substitute.For(); + + testSubject = new ServerBranchProvider(activeSolutionBoundTracker, gitWorkspaceService, branchMatcher, logger, createRepoOp); + } + + [TestMethod] + public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); - } [TestMethod] - public async Task Get_StandaloneMode_ReturnsNull() + public void Get_StandaloneMode_ReturnsNull() { - var configProvider = CreateConfigProvider(CreateBindingConfig(SonarLintMode.Standalone)); - var gitWorkspace = new Mock(); - var branchMatcher = new Mock(); - var logger = new TestLogger(logToConsole: true); - var sonarQubeService = CreateSonarQubeService(); + SetUpBindingConfiguration(SonarLintMode.Standalone); + var branches = new List { new RemoteBranch("branch1", false), new RemoteBranch("main", true), new RemoteBranch("branch2", false) }; - var testSubject = CreateTestSubject(configProvider.Object, - gitWorkspace.Object, - branchMatcher: branchMatcher.Object, - sonarQubeService: sonarQubeService.Object, - logger: logger); - - var actual = await testSubject.GetServerBranchNameAsync(CancellationToken.None); + var actual = testSubject.GetServerBranchName(branches); actual.Should().BeNull(); - - configProvider.VerifyAll(); - gitWorkspace.Invocations.Should().HaveCount(0); - branchMatcher.Invocations.Should().HaveCount(0); - sonarQubeService.Invocations.Should().HaveCount(0); + _ = activeSolutionBoundTracker.Received(1).CurrentConfiguration; + gitWorkspaceService.DidNotReceiveWithAnyArgs().GetRepoRoot(); + branchMatcher.DidNotReceiveWithAnyArgs().GetMatchingBranch(Arg.Any(), Arg.Any(), Arg.Any>()); } [TestMethod] - public async Task Get_NoGitRepo_ReturnsDefaultMainBranch() + public void Get_NoGitRepo_ReturnsDefaultMainBranch() { - var configProvider = CreateConfigProvider(CreateBindingConfig(SonarLintMode.Connected)); - var gitWorkspace = CreateGitWorkspace(repoRootToReturn: null); - - var branchMatcher = new Mock(); - var logger = new TestLogger(logToConsole: true); - var createRepoOp = CreateCreateRepoOp(repository: null); - - var sonarQubeService = CreateSonarQubeService(mainBranchName: "main branch name"); + SetUpBindingConfiguration(SonarLintMode.Connected); + SetUpGitWorkspaceService(null); + createRepoOp.Invoke(Arg.Any()).Returns((IRepository)null); + var branches = new List { new RemoteBranch("branch1", false), new RemoteBranch("main branch name", true), new RemoteBranch("branch2", false) }; - var testSubject = CreateTestSubject(configProvider.Object, - gitWorkspace.Object, - sonarQubeService: sonarQubeService.Object, - branchMatcher: branchMatcher.Object, - logger: logger, - createRepoOp: createRepoOp.Object); - - var actual = await testSubject.GetServerBranchNameAsync(CancellationToken.None); + var actual = testSubject.GetServerBranchName(branches); actual.Should().Be("main branch name"); - - configProvider.VerifyAll(); - gitWorkspace.Verify(x => x.GetRepoRoot(), Times.Once); - gitWorkspace.Invocations.Should().HaveCount(1); - branchMatcher.Invocations.Should().HaveCount(0); - createRepoOp.Invocations.Should().HaveCount(0); + _ = activeSolutionBoundTracker.Received(1).CurrentConfiguration; + gitWorkspaceService.Received(1).GetRepoRoot(); + gitWorkspaceService.ReceivedCalls().Should().HaveCount(1); + branchMatcher.DidNotReceiveWithAnyArgs().GetMatchingBranch(Arg.Any(), Arg.Any(), Arg.Any>()); + createRepoOp.DidNotReceiveWithAnyArgs().Invoke(Arg.Any()); } [TestMethod] [DataRow(SonarLintMode.LegacyConnected)] [DataRow(SonarLintMode.Connected)] - public async Task Get_ConnectedModeAndHasGitRepo_HasMatchingBranch_ReturnsExpectedBranch(SonarLintMode mode) + public void Get_ConnectedModeAndHasGitRepo_HasMatchingBranch_ReturnsExpectedBranch(SonarLintMode mode) { - var configProvider = CreateConfigProvider(CreateBindingConfig(mode, "my project key")); - var gitWorkspace = CreateGitWorkspace("c:\\aaa\\reporoot"); - - var branchMatcher = CreateBranchMatcher(branchToReturn: "my matching branch"); - var logger = new TestLogger(logToConsole: true); - - var repo = Mock.Of(); - var createRepoOp = CreateCreateRepoOp(repository: repo); - var sonarQubeService = CreateSonarQubeService(); + SetUpBindingConfiguration(mode, "my project key"); + var repoRoot = "c:\\aaa\\reporoot"; + SetUpGitWorkspaceService(repoRoot); + SetUpBranchMatcher("my matching branch"); + createRepoOp.Invoke(repoRoot).Returns(repo); + var branches = new List { new RemoteBranch("branch1", false), new RemoteBranch("main", true), new RemoteBranch("branch2", false) }; - var testSubject = CreateTestSubject(configProvider.Object, - gitWorkspace.Object, - branchMatcher: branchMatcher.Object, - sonarQubeService: sonarQubeService.Object, - logger: logger, - createRepoOp: createRepoOp.Object); - - var actual = await testSubject.GetServerBranchNameAsync(CancellationToken.None); + var actual = testSubject.GetServerBranchName(branches); actual.Should().Be("my matching branch"); logger.AssertPartialOutputStringExists("my matching branch"); - - configProvider.VerifyAll(); - gitWorkspace.Verify(x => x.GetRepoRoot(), Times.Once); - createRepoOp.Verify(x => x.Invoke("c:\\aaa\\reporoot"), Times.Once); - branchMatcher.Verify(x => x.GetMatchingBranch("my project key", repo, It.IsAny()), Times.Once); - - gitWorkspace.Invocations.Should().HaveCount(1); - branchMatcher.Invocations.Should().HaveCount(1); - createRepoOp.Invocations.Should().HaveCount(1); - sonarQubeService.Invocations.Should().HaveCount(0); + _ = activeSolutionBoundTracker.Received(1).CurrentConfiguration; + gitWorkspaceService.Received(1).GetRepoRoot(); + createRepoOp.Received(1).Invoke(repoRoot); + branchMatcher.Received(1).GetMatchingBranch("my project key", repo, branches); } [TestMethod] - public async Task Get_ConnectedModeAndHasGitRepo_NoMatchingBranch_ReturnsDefaultMainBranch() + public void Get_ConnectedModeAndHasGitRepo_NoMatchingBranch_ReturnsDefaultMainBranch() { - var configProvider = CreateConfigProvider(CreateBindingConfig(SonarLintMode.Connected, "my project key")); - var gitWorkspace = CreateGitWorkspace("x:\\"); - - var branchMatcher = CreateBranchMatcher(branchToReturn: null); - var logger = new TestLogger(logToConsole: true); + SetUpBindingConfiguration(SonarLintMode.Connected, "my project key"); + var repoRoot = "x:\\"; + SetUpGitWorkspaceService(repoRoot); + SetUpBranchMatcher(null); + createRepoOp.Invoke(repoRoot).Returns(repo); + var branches = new List { new RemoteBranch("branch1", false), new RemoteBranch("some main branch", true), new RemoteBranch("branch2", false) }; - var repo = Mock.Of(); - var createRepoOp = CreateCreateRepoOp(repository: repo); - var sonarQubeService = CreateSonarQubeService(mainBranchName: "some main branch"); - - var testSubject = CreateTestSubject(configProvider.Object, - gitWorkspace.Object, - branchMatcher: branchMatcher.Object, - sonarQubeService: sonarQubeService.Object, - logger: logger, - createRepoOp: createRepoOp.Object); - - var actual = await testSubject.GetServerBranchNameAsync(CancellationToken.None); + var actual = testSubject.GetServerBranchName(branches); actual.Should().Be("some main branch"); - - branchMatcher.Verify(x => x.GetMatchingBranch("my project key", repo, It.IsAny()), Times.Once); - } - - private static ServerBranchProvider CreateTestSubject( - IConfigurationProvider configurationProvider = null, - IGitWorkspaceService gitWorkspaceService = null, - ISonarQubeService sonarQubeService = null, - IBranchMatcher branchMatcher = null, - ILogger logger = null, - ServerBranchProvider.CreateRepositoryObject createRepoOp = null) - { - configurationProvider ??= Mock.Of(); - gitWorkspaceService ??= Mock.Of(); - sonarQubeService ??= Mock.Of(); - branchMatcher ??= Mock.Of(); - logger ??= new TestLogger(); - createRepoOp ??= (string repoRoot) => null; - - var testSubject = new ServerBranchProvider(configurationProvider, gitWorkspaceService, sonarQubeService, branchMatcher, logger, createRepoOp); - return testSubject; - } - - private static BindingConfiguration CreateBindingConfig(SonarLintMode mode = SonarLintMode.Connected, string projectKey = "any") - => new(new BoundServerProject("solution", projectKey, new ServerConnection.SonarCloud("org")), mode, "any dir"); - - private static Mock CreateConfigProvider(BindingConfiguration config = null) - { - config ??= CreateBindingConfig(); - - var configProvider = new Mock(); - configProvider.Setup(x => x.GetConfiguration()).Returns(config); - return configProvider; + branchMatcher.Received(1).GetMatchingBranch("my project key", repo, branches); } - private static Mock CreateGitWorkspace(string repoRootToReturn) + private void SetUpBindingConfiguration(SonarLintMode mode = SonarLintMode.Connected, string projectKey = "any") { - var gitWorkspace = new Mock(); - gitWorkspace.Setup(x => x.GetRepoRoot()).Returns(repoRootToReturn); - return gitWorkspace; - } + var config = new BindingConfiguration( + new BoundServerProject("solution", projectKey, new ServerConnection.SonarCloud("org")), + mode, + "any dir"); - private static Mock CreateCreateRepoOp(IRepository repository) - { - var createOp = new Mock(); - createOp.Setup(x => x.Invoke(It.IsAny())).Returns(repository); - return createOp; + activeSolutionBoundTracker.CurrentConfiguration.Returns(config); } - private static Mock CreateBranchMatcher(string branchToReturn) + private void SetUpGitWorkspaceService(string repoRootToReturn) { - var branchMatcher = new Mock(); - branchMatcher.Setup(x => x.GetMatchingBranch(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(branchToReturn); - return branchMatcher; + gitWorkspaceService.GetRepoRoot().Returns(repoRootToReturn); } - private Mock CreateSonarQubeService(string mainBranchName = "some branch") + private void SetUpBranchMatcher(string branchToReturn) { - var sonarQubeService = new Mock(); - - var serverBranches = new[] - { - new SonarQubeProjectBranch(Guid.NewGuid().ToString(), false, default, "BRANCH"), - new SonarQubeProjectBranch(mainBranchName, true, default, "BRANCH"), - new SonarQubeProjectBranch(Guid.NewGuid().ToString(), false, default, "BRANCH") - }; - - sonarQubeService - .Setup(x => x.GetProjectBranchesAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(serverBranches); - - return sonarQubeService; + branchMatcher.GetMatchingBranch(Arg.Any(), Arg.Any(), Arg.Any>()) + .Returns(branchToReturn); } } } diff --git a/src/ConnectedMode.UnitTests/ServerIssueFinderTests.cs b/src/ConnectedMode.UnitTests/ServerIssueFinderTests.cs deleted file mode 100644 index 48d2f15c2a..0000000000 --- a/src/ConnectedMode.UnitTests/ServerIssueFinderTests.cs +++ /dev/null @@ -1,178 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Threading; -using System.Threading.Tasks; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.Core.Suppressions; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client; -using SonarQube.Client.Helpers; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests; - -[TestClass] -public class ServerIssueFinderTests -{ - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void MefCtor_CheckIsSingleton() - { - MefTestHelpers.CheckIsSingletonMefComponent(); - } - - [TestMethod] - public void FindServerIssueAsync_UIThread_Throws() - { - var serverIssueFinder = CreateTestSubject(out _, out _, out _, out _, out _, out var threadHandlingMock); - var exception = new Exception(); - threadHandlingMock.Setup(x => x.ThrowIfOnUIThread()).Throws(exception); - - Func act = async () => await serverIssueFinder.FindServerIssueAsync(Mock.Of(), CancellationToken.None); - - act.Should().Throw().Which.Should().BeSameAs(exception); - } - - [TestMethod] - public async Task FindServerIssueAsync_StandaloneMode_ReturnsNull() - { - var serverIssueFinder = CreateTestSubject(out _, out _, out var activeSolutionBoundTrackerMock, out _, out _, out _); - SetUpStandalone(activeSolutionBoundTrackerMock); - - var result = await serverIssueFinder.FindServerIssueAsync(Mock.Of(), CancellationToken.None); - - result.Should().BeNull(); - } - - [TestMethod] - public async Task FindServerIssueAsync_RootCantBeCalculated_ReturnsNull() - { - const string filePath = @"c:\a\b\c"; - - var serverIssueFinder = CreateTestSubject(out var projectRootCalculatorMock, out _, out var activeSolutionBoundTrackerMock, out _, out _, out _); - SetUpBinding(activeSolutionBoundTrackerMock, "project"); - projectRootCalculatorMock.Setup(x => x.CalculateBasedOnLocalPathAsync(filePath, It.IsAny())) - .ReturnsAsync((string)null); - - var result = await serverIssueFinder.FindServerIssueAsync(CreateIssue("rule", filePath), CancellationToken.None); - - result.Should().BeNull(); - } - - [DataTestMethod] - [DataRow(true)] - [DataRow(false)] - public async Task FindServerIssueAsync_ReturnsMatchingServerIssueIfMatched(bool isMatch) - { - const string ruleId = "rule"; - const string filePath = @"c:\a\b\c"; - const string root = @"c:\a\"; - const string projectKey = "project"; - const string branch = "branch123"; - var localIssue = CreateIssue(ruleId, filePath); - var serverIssues = new[] { CreateServerIssue(), CreateServerIssue() }; - - var serverIssueFinder = CreateTestSubject(out var projectRootCalculatorMock, - out var issueMatcherMock, - out var activeSolutionBoundTrackerMock, - out var statefulServerBranchProviderMock, - out var sonarQubeServiceMock, - out _); - SetUpBinding(activeSolutionBoundTrackerMock, projectKey); - projectRootCalculatorMock - .Setup(x => x.CalculateBasedOnLocalPathAsync(filePath, It.IsAny())) - .ReturnsAsync(root); - statefulServerBranchProviderMock - .Setup(x => x.GetServerBranchNameAsync(It.IsAny())) - .ReturnsAsync(branch); - sonarQubeServiceMock - .Setup(x => x.GetIssuesForComponentAsync(projectKey, branch, ComponentKeyGenerator.GetComponentKey(filePath, root, projectKey), ruleId, It.IsAny())) - .ReturnsAsync(serverIssues); - issueMatcherMock - .Setup(x => x.GetFirstLikelyMatchFromSameFileOrNull(localIssue, serverIssues)) - .Returns(isMatch ? serverIssues[1] : null); - - var result = await serverIssueFinder.FindServerIssueAsync(localIssue, CancellationToken.None); - - if (isMatch) - { - result.Should().BeSameAs(serverIssues[1]); - } - else - { - result.Should().BeNull(); - } - } - - private SonarQubeIssue CreateServerIssue() - { - return new SonarQubeIssue("test", "test", "test", "test", "test", "test", false, SonarQubeIssueSeverity.Info, - DateTimeOffset.MinValue, DateTimeOffset.MinValue, null, null); - } - - private IFilterableIssue CreateIssue(string ruleId, string filePath, int? startLine = null, string lineHash = null) => - new TestFilterableIssue - { - RuleId = ruleId, - FilePath = filePath, - StartLine = startLine, - LineHash = lineHash - }; - - private void SetUpStandalone(Mock activeSolutionBoundTrackerMock) => - SetUpBinding(activeSolutionBoundTrackerMock, null); - - private void SetUpBinding(Mock activeSolutionBoundTrackerMock, string projectKey) - { - activeSolutionBoundTrackerMock.SetupGet(x => x.CurrentConfiguration) - .Returns(projectKey == null - ? BindingConfiguration.Standalone - : new BindingConfiguration(new BoundServerProject("solution", projectKey, new ServerConnection.SonarQube(new Uri("http://localhost"))), SonarLintMode.Connected, default)); - } - - private ServerIssueFinder CreateTestSubject(out Mock projectRootCalculatorMock, - out Mock issueMatcherMock, - out Mock activeSolutionBoundTrackerMock, - out Mock statefulServerBranchProviderMock, - out Mock sonarQubeServiceMock, - out Mock threadHandlingMock) - { - return new ServerIssueFinder((projectRootCalculatorMock = new Mock(MockBehavior.Strict)).Object, - (issueMatcherMock = new Mock(MockBehavior.Strict)).Object, - (activeSolutionBoundTrackerMock = new Mock(MockBehavior.Strict)).Object, - (statefulServerBranchProviderMock = new Mock(MockBehavior.Strict)).Object, - (sonarQubeServiceMock = new Mock(MockBehavior.Strict)).Object, - (threadHandlingMock = new Mock()).Object); // not strict since does not affect logic - } -} diff --git a/src/ConnectedMode.UnitTests/ServerQueryInfoProviderTests.cs b/src/ConnectedMode.UnitTests/ServerQueryInfoProviderTests.cs deleted file mode 100644 index 01478f4035..0000000000 --- a/src/ConnectedMode.UnitTests/ServerQueryInfoProviderTests.cs +++ /dev/null @@ -1,135 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Threading; -using System.Threading.Tasks; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.TestInfrastructure; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests -{ - [TestClass] - public class ServerQueryInfoProviderTests - { - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - [DataRow(SonarLintMode.Connected)] - [DataRow(SonarLintMode.LegacyConnected)] - public async Task Get_IsConnectedMode_ReturnsExpectedValues(SonarLintMode mode) - { - // Happy path - in connected mode, has branch - var config = CreateBindingConfig(mode, "my project key"); - var configProvider = CreateConfigProvider(config); - - var branchProvider = CreateBranchProvider("my branch"); - - var testSubject = CreateTestSubject(configProvider.Object, branchProvider.Object); - - var actual = await testSubject.GetProjectKeyAndBranchAsync(CancellationToken.None); - - actual.projectKey.Should().Be("my project key"); - actual.branchName.Should().Be("my branch"); - } - - [TestMethod] - public async Task Get_IsStandaloneMode_ReturnsNulls() - { - var config = CreateBindingConfig(SonarLintMode.Standalone, "my project key"); - var configProvider = CreateConfigProvider(config); - - var testSubject = CreateTestSubject(configProvider.Object); - - var actual = await testSubject.GetProjectKeyAndBranchAsync(CancellationToken.None); - - actual.projectKey.Should().BeNull(); - actual.branchName.Should().BeNull(); - } - - [TestMethod] - public void Get_OperationIsCancelled_ThrowsOperationCancelledException() - { - // Tests that the class doesn't squash OperationCancelledExceptions - // i.e. they are the callers responsibility - - var config = CreateBindingConfig(SonarLintMode.Connected, "my project key"); - var configProvider = CreateConfigProvider(config); - - var cancellationTokenSource = new CancellationTokenSource(); - - var branchProvider = new Mock(); - branchProvider.Setup(x => x.GetServerBranchNameAsync(cancellationTokenSource.Token)). - Callback(() => - { - // Simulate what happens when the cancellation token is cancelled - cancellationTokenSource.Cancel(); - cancellationTokenSource.Token.ThrowIfCancellationRequested(); - }); - - var testSubject = CreateTestSubject(configProvider.Object, branchProvider.Object); - - Func operation = () => testSubject.GetProjectKeyAndBranchAsync(cancellationTokenSource.Token); - - operation.Should().Throw(); - } - - - private static ServerQueryInfoProvider CreateTestSubject(IConfigurationProvider configurationProvider = null, - IStatefulServerBranchProvider serverBranchProvider = null) - { - configurationProvider ??= Mock.Of(); - serverBranchProvider ??= Mock.Of(); - - var testSubject = new ServerQueryInfoProvider(configurationProvider, serverBranchProvider); - return testSubject; - } - - private static BindingConfiguration CreateBindingConfig(SonarLintMode mode = SonarLintMode.Connected, string projectKey = "any") - => new(new BoundServerProject("solution", projectKey, new ServerConnection.SonarCloud("org")), mode, "any dir"); - - private static Mock CreateConfigProvider(BindingConfiguration config = null) - { - config ??= CreateBindingConfig(); - - var configProvider = new Mock(); - configProvider.Setup(x => x.GetConfiguration()).Returns(config); - return configProvider; - } - - private static Mock CreateBranchProvider(string branchName) - => CreateBranchProvider(branchName, CancellationToken.None); - - private static Mock CreateBranchProvider(string branchName, - CancellationToken cancellationToken) - { - var mock = new Mock(); - mock.Setup(x => x.GetServerBranchNameAsync(cancellationToken)).ReturnsAsync(branchName); - return mock; - } - } -} diff --git a/src/ConnectedMode.UnitTests/ServerSentEvents/Issue/IssueServerEventsListenerTests.cs b/src/ConnectedMode.UnitTests/ServerSentEvents/Issue/IssueServerEventsListenerTests.cs deleted file mode 100644 index 0c61b698e2..0000000000 --- a/src/ConnectedMode.UnitTests/ServerSentEvents/Issue/IssueServerEventsListenerTests.cs +++ /dev/null @@ -1,199 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Issue; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.ServerSentEvents.Issue -{ - [TestClass] - public class IssueServerEventsListenerTests - { - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public async Task OnEvent_SingleEvent_MultipleTuples_StoreIsUpdatedWithIssuesFromCurrentBranch() - { - var event1 = CreateServerEvent(isResolved: true, - new BranchAndIssueKey("issueKey1", "branch1"), - new BranchAndIssueKey("issueKey2", "branch2"), - new BranchAndIssueKey("issueKey3", "branch1")); - - var issueServerEventSource = SetupIssueServerEventSource(event1); - var suppressionsUpdater = new Mock(); - var branchProvider = CreateBranchProvider("branch1"); - - var testSubject = CreateTestSubject(issueServerEventSource.Object, suppressionsUpdater.Object, branchProvider.Object); - - await testSubject.ListenAsync(); - - issueServerEventSource.Verify(x => x.GetNextEventOrNullAsync(), Times.Exactly(2)); - issueServerEventSource.VerifyNoOtherCalls(); - - suppressionsUpdater.Verify(x => x.UpdateSuppressedIssuesAsync( - true, - new[] { "issueKey1", "issueKey3" }, - It.IsAny()), - Times.Once); - suppressionsUpdater.VerifyNoOtherCalls(); - } - - [TestMethod] - public async Task OnEvent_MultipleEvents_StoreIsUpdatedWithIssuesFromCurrentBranch() - { - var event1 = CreateServerEvent(isResolved: true, new BranchAndIssueKey("issueKey1", "branch1")); - var event2 = CreateServerEvent(isResolved: true, new BranchAndIssueKey("issueKey2", "branch2")); - var event3 = CreateServerEvent(isResolved: false, new BranchAndIssueKey("issueKey3", "branch1")); - - var issueServerEventSource = SetupIssueServerEventSource(event1, event2, event3); - var suppressionUpdater = new Mock(); - var branchProvider = CreateBranchProvider("branch1"); - - var testSubject = CreateTestSubject(issueServerEventSource.Object, suppressionUpdater.Object, branchProvider.Object); - - await testSubject.ListenAsync(); - - issueServerEventSource.Verify(x => x.GetNextEventOrNullAsync(), Times.Exactly(4)); - issueServerEventSource.VerifyNoOtherCalls(); - - suppressionUpdater.Verify(x => x.UpdateSuppressedIssuesAsync( - true, - new[] { "issueKey1" }, - It.IsAny()), - Times.Once); - - suppressionUpdater.Verify(x => x.UpdateSuppressedIssuesAsync( - false, - new[] { "issueKey3" }, - It.IsAny()), - Times.Once); - - suppressionUpdater.VerifyNoOtherCalls(); - } - - [TestMethod] - public void OnEvent_FailureToProcessIssueEvent_CriticalException_StopsListeningToServerEventsSource() - { - var issueServerEventSource = new Mock(); - issueServerEventSource - .Setup(x => x.GetNextEventOrNullAsync()) - .Throws(new StackOverflowException("this is a test")); - - var testSubject = CreateTestSubject(issueServerEventSource: issueServerEventSource.Object); - - Func func = async () => await testSubject.ListenAsync(); - - func.Should().Throw().And.Message.Should().Be("this is a test"); - issueServerEventSource.Verify(x => x.GetNextEventOrNullAsync(), Times.Once); - issueServerEventSource.VerifyNoOtherCalls(); - } - - [TestMethod] - public async Task Dispose_StopsListeningToServerEventsSource() - { - var issueServerEventSource = SetupIssueServerEventSource(); - var testSubject = CreateTestSubject(issueServerEventSource: issueServerEventSource.Object); - - var listenTask = testSubject.ListenAsync(); - testSubject.Dispose(); - - await listenTask; - - issueServerEventSource.Verify(x => x.GetNextEventOrNullAsync(), Times.Once); - } - - [TestMethod] - [Description("Regression test for https://github.com/SonarSource/sonarlint-visualstudio/issues/3946")] - public void Dispose_CalledASecondTime_NoException() - { - var testSubject = CreateTestSubject(); - - testSubject.Dispose(); - - Action act = () => testSubject.Dispose(); - act.Should().NotThrow(); - } - - private static Mock SetupIssueServerEventSource(params IIssueChangedServerEvent[] serverEvents) - { - var issueServerEventSource = new Mock(); - - var sequence = issueServerEventSource.SetupSequence(x => x.GetNextEventOrNullAsync()); - - foreach (var serverEvent in serverEvents) - { - sequence.ReturnsAsync(serverEvent); - } - - // Signal that the task is finished - sequence.ReturnsAsync((IIssueChangedServerEvent)null); - - return issueServerEventSource; - } - - private static IssueServerEventsListener CreateTestSubject( - IIssueServerEventSource issueServerEventSource = null, - IRoslynSuppressionUpdater roslynSuppressionUpdater = null, - IStatefulServerBranchProvider branchProvider = null, - IThreadHandling threadHandling = null, - ILogger logger = null) - { - issueServerEventSource ??= Mock.Of(); - roslynSuppressionUpdater ??= Mock.Of(); - branchProvider ??= Mock.Of(); - threadHandling ??= new NoOpThreadHandler(); - logger ??= Mock.Of(); - - return new IssueServerEventsListener(issueServerEventSource, roslynSuppressionUpdater, branchProvider, threadHandling, logger); - } - - private static IIssueChangedServerEvent CreateServerEvent(bool isResolved, params BranchAndIssueKey[] branchAndIssueKeys) - { - var serverEvent = new Mock(); - serverEvent.Setup(x => x.IsResolved).Returns(isResolved); - serverEvent.Setup(x => x.BranchAndIssueKeys).Returns(branchAndIssueKeys); - - return serverEvent.Object; - } - - private Mock CreateBranchProvider(string serverBranch) - { - var branchProvider = new Mock(); - - branchProvider - .Setup(x => x.GetServerBranchNameAsync(It.IsAny())) - .ReturnsAsync(serverBranch); - - return branchProvider; - } - } -} diff --git a/src/ConnectedMode.UnitTests/ServerSentEvents/QualityProfile/QualityProfileServerEventsListenerTests.cs b/src/ConnectedMode.UnitTests/ServerSentEvents/QualityProfile/QualityProfileServerEventsListenerTests.cs deleted file mode 100644 index 5c89be3827..0000000000 --- a/src/ConnectedMode.UnitTests/ServerSentEvents/QualityProfile/QualityProfileServerEventsListenerTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.QualityProfile; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.ServerSentEvents.QualityProfile; - -[TestClass] -public class QualityProfileServerEventsListenerTests -{ - [TestMethod] - public void MefCtor_CheckExports() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void MefCtor_CheckIsSingleton() - { - MefTestHelpers.CheckIsSingletonMefComponent(); - } - - [TestMethod] - public async Task ListenAsync_CallsUpdaterForEachEventUntilNullEvent() - { - var events = new[] - { - Task.FromResult(Mock.Of()), - Task.FromResult(Mock.Of()), - Task.FromResult(Mock.Of()), - Task.FromResult(null), // should stop on null no matter what - Task.FromResult(Mock.Of()), - }; - var eventSourceMock = SetUpEventSourceSequence(events); - var threadHandlingMock = SetUpThreadHandlingSwitchToBackgroundThread(); - var updaterMock = new Mock(); - - var testSubject = new QualityProfileServerEventsListener(eventSourceMock.Object, updaterMock.Object, threadHandlingMock.Object); - - await testSubject.ListenAsync(); - - eventSourceMock.Verify(x => x.GetNextEventOrNullAsync(), Times.Exactly(events.Length - 1 /* the event after null is ignored */)); - updaterMock.Verify(x => x.UpdateAsync(), Times.Exactly(events.Length - 1/* null doesn't trigger an update */ - 1/* the event after null is ignored */)); - threadHandlingMock.Verify(th => th.SwitchToBackgroundThread(), Times.Once); - } - - private static Mock SetUpThreadHandlingSwitchToBackgroundThread() - { - var threadHandlingMock = new Mock(); - threadHandlingMock.Setup(th => th.SwitchToBackgroundThread()).Returns(new NoOpThreadHandler.NoOpAwaitable()); - return threadHandlingMock; - } - - private static Mock SetUpEventSourceSequence(Task[] events) - { - var currentEventIndex = 0; - var eventSourceMock = new Mock(); - eventSourceMock.Setup(x => x.GetNextEventOrNullAsync()).Returns(() => events[currentEventIndex++]); - return eventSourceMock; - } -} diff --git a/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionFactoryTests.cs b/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionFactoryTests.cs deleted file mode 100644 index 9eb7bccdba..0000000000 --- a/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionFactoryTests.cs +++ /dev/null @@ -1,87 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client; -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Issue; -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.QualityProfile; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.ServerSentEvents; - -[TestClass] -public class SSESessionFactoryTests -{ - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void Create_ReturnsCorrectType() - { - var testSubject = CreateTestSubject(); - - var sseSession = testSubject.Create("MyProjectName", null); - - sseSession.Should().NotBeNull().And.BeOfType(); - } - - [TestMethod] - public void Create_AfterDispose_Throws() - { - var testSubject = CreateTestSubject(); - - testSubject.Dispose(); - Action act = () => testSubject.Create("MyProjectName", null); - - act.Should().Throw(); - } - - [TestMethod] - public void Dispose_IdempotentAndDisposesPublishers() - { - var issuesPublisherMock = new Mock(); - var qualityProfilePublisherMock = new Mock(); - var testSubject = CreateTestSubject(issuesPublisherMock, qualityProfilePublisherMock); - - testSubject.Dispose(); - testSubject.Dispose(); - testSubject.Dispose(); - - issuesPublisherMock.Verify(p => p.Dispose(), Times.Once); - qualityProfilePublisherMock.Verify(p => p.Dispose(), Times.Once); - } - - private SSESessionFactory CreateTestSubject(Mock issuePublisher = null, - Mock qualityProfileServerEventSourcePublisher = null) => - new(Mock.Of(), - issuePublisher?.Object ?? Mock.Of(), - qualityProfileServerEventSourcePublisher?.Object ?? Mock.Of(), - Mock.Of(), - Mock.Of()); -} diff --git a/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionManagerTests.cs b/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionManagerTests.cs deleted file mode 100644 index 1a02131ed2..0000000000 --- a/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionManagerTests.cs +++ /dev/null @@ -1,340 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.TestInfrastructure; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.ServerSentEvents; - -[TestClass] -public class SSESessionManagerTests -{ - private const string DefaultProjectKey = "myproj"; - - [TestMethod] - public void MefCtor_CheckIsExported() - { - var activeSolutionBoundTrackerMock = new Mock(); - activeSolutionBoundTrackerMock.SetupGet(tracker => tracker.CurrentConfiguration) - .Returns(BindingConfiguration.Standalone); - - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(activeSolutionBoundTrackerMock.Object), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void Ctor_DoesNotCallAnyServices_BesidesExpected() - { - var activeSolutionBoundTracker = new Mock(); - var sseSessionFactory = new Mock(); - var logger = new Mock(); - - var _ = new SSESessionManager(activeSolutionBoundTracker.Object, sseSessionFactory.Object, logger.Object); - - // The MEF constructor should be free-threaded, which it will be if - // it doesn't make any external calls. - - activeSolutionBoundTracker.VerifyAdd(tracker => tracker.SolutionBindingChanged += It.IsAny>(), Times.Once); - activeSolutionBoundTracker.VerifyNoOtherCalls(); - sseSessionFactory.Invocations.Should().BeEmpty(); - logger.Invocations.Should().BeEmpty(); - } - - [TestMethod] - public void Ctor_SubscribesToBindingChangedEvent() - { - var testScope = new TestScope(); - - var _ = testScope.CreateTestSubject(); - - testScope.ActiveSolutionBoundTrackerMock.VerifyAdd( - tracker => tracker.SolutionBindingChanged += - It.IsAny>(), Times.Once); - } - - [TestMethod] - public void CreateSessionIfInConnectedMode_WhenInStandaloneModeOnCreation_DoesNotCreateSession() - { - var bindingConfig = BindingConfiguration.Standalone; - var testScope = new TestScope(bindingConfig); - - var _ = testScope.CreateTestSubject(); - var testSubject = testScope.CreateTestSubject(); - - testSubject.CreateSessionIfInConnectedMode(bindingConfig); - - testScope.SSESessionFactoryMock.Verify(factory => factory.Create(DefaultProjectKey, It.IsAny()), Times.Never); - } - - [TestMethod] - public void CreateSessionIfInConnectedMode_WhenConnectedToSonarCloud_DoesNotCreateSession() - { - var bindingConfig = TestScope.CreateConnectedModeSonarCloudBindingConfiguration(DefaultProjectKey); - - var testScope = new TestScope(bindingConfig); - - var _ = testScope.CreateTestSubject(); - var testSubject = testScope.CreateTestSubject(); - - testSubject.CreateSessionIfInConnectedMode(bindingConfig); - - testScope.SSESessionFactoryMock.Verify(factory => factory.Create(DefaultProjectKey, It.IsAny()), Times.Never); - } - - [TestMethod] - public void CreateSessionIfInConnectedMode_WhenInConnectedModeOnCreation_CreatesSession() - { - var bindingConfig = TestScope.CreateConnectedModeSonarQubeBindingConfiguration(DefaultProjectKey); - - var testScope = new TestScope(bindingConfig); - var sessionMock = testScope.SetUpSSEFactoryToReturnNoOpSSESession(DefaultProjectKey); - - var testSubject = testScope.CreateTestSubject(); - - testSubject.CreateSessionIfInConnectedMode(bindingConfig); - testScope.SSESessionFactoryMock.Verify(factory => factory.Create(DefaultProjectKey, It.IsAny()), Times.Once); - sessionMock.Verify(session => session.PumpAllAsync(), Times.Once); - } - - [TestMethod] - public void OnSolutionChanged_WhenChangesFromStandaloneToConnected_CreatesSessionAndLaunchesIt() - { - var testScope = new TestScope(); - var _ = testScope.CreateTestSubject(); - testScope.RaiseInStandaloneModeEvent(); - var sessionMock = testScope.SetUpSSEFactoryToReturnNoOpSSESession(DefaultProjectKey); - - testScope.RaiseInConnectedModeEvent(DefaultProjectKey); - - testScope.SSESessionFactoryMock.Verify(factory => factory.Create(DefaultProjectKey, It.IsAny()), Times.Once); - sessionMock.Verify(session => session.PumpAllAsync(), Times.Once); - } - - [TestMethod] - public void OnSolutionChanged_WhenChangesFromConnectedToStandalone_DisposesPreviousSession() - { - var testScope = new TestScope(); - var _ = testScope.CreateTestSubject(); - var sessionMock = testScope.SetUpSSEFactoryToReturnNoOpSSESession(DefaultProjectKey); - sessionMock.Setup(session => session.Dispose()); - testScope.RaiseInConnectedModeEvent(DefaultProjectKey); - testScope.SSESessionFactoryMock.Invocations.Clear(); - - testScope.RaiseInStandaloneModeEvent(); - - testScope.SSESessionFactoryMock.Verify(factory => factory.Create(It.IsAny(), It.IsAny()), Times.Never); - sessionMock.Verify(session => session.Dispose(), Times.Once); - } - - [DataTestMethod] - [DataRow(true)] - [DataRow(false)] - public void OnSolutionChanged_WhenChangesFromConnectedToConnected_CancelsSessionAndStartsNewOne(bool sameProjectKey) - { - var projectKey1 = "proj1"; - var projectKey2 = sameProjectKey ? projectKey1 : "proj2"; - var testScope = new TestScope(); - var _ = testScope.CreateTestSubject(); - var sessionMock1 = testScope.SetUpSSEFactoryToReturnNoOpSSESession(projectKey1); - sessionMock1.Setup(session => session.Dispose()); - testScope.RaiseInConnectedModeEvent(projectKey1); - var sessionMock2 = testScope.SetUpSSEFactoryToReturnNoOpSSESession(projectKey2); - - testScope.RaiseInConnectedModeEvent(projectKey2); - - sessionMock1.Verify(session => session.Dispose(), Times.Once); - testScope.SSESessionFactoryMock.Verify(factory => factory.Create(projectKey2, It.IsAny()), Times.Exactly(sameProjectKey ? 2 : 1)); - sessionMock2.Verify(session => session.Dispose(), Times.Never); - } - - [DataTestMethod] - public async Task OnSessionFailed_CancelsSessionAndStartsNewOne() - { - var bindingConfig = TestScope.CreateConnectedModeSonarQubeBindingConfiguration(DefaultProjectKey); - - var testScope = new TestScope(bindingConfig); - - var sessionMock1 = testScope.SetUpSSEFactoryToReturnNoOpSSESession(DefaultProjectKey); - sessionMock1.Setup(session => session.Dispose()); - var testSubject = testScope.CreateTestSubject(); - testSubject.CreateSessionIfInConnectedMode(bindingConfig); - - var sessionMock2 = testScope.SetUpSSEFactoryToReturnNoOpSSESession(DefaultProjectKey); - - await testScope.CapturedSessionFailedCallback(sessionMock1.Object); - - sessionMock1.Verify(session => session.Dispose(), Times.Once); - testScope.SSESessionFactoryMock.Verify(factory => factory.Create(DefaultProjectKey, It.IsAny()), Times.Exactly(2)); - sessionMock2.Verify(session => session.Dispose(), Times.Never); - } - - [TestMethod] - public void Dispose_WhenInConnectedMode_CorrectAndIdempotent() - { - var testScope = new TestScope(); - var testSubject = testScope.CreateTestSubject(); - var sseSession = testScope.SetUpSSEFactoryToReturnNoOpSSESession(DefaultProjectKey); - testScope.SetUpCorrectDisposeOrder(sseSession); - testScope.RaiseInConnectedModeEvent(DefaultProjectKey); - - CallDisposeMultipleTimes(testSubject); - - VerifyUnsubscribedFromBindingChangedEvent(testScope); - VerifySSESessionFactoryDisposedOnce(testScope); - sseSession.Verify(session => session.Dispose(), Times.Once); - } - - [TestMethod] - public void Dispose_WhenInStandaloneMode_CorrectAndIdempotent() - { - var testScope = new TestScope(); - var testSubject = testScope.CreateTestSubject(); - testScope.SetUpCorrectDisposeOrder(null); - testScope.RaiseInStandaloneModeEvent(); - - CallDisposeMultipleTimes(testSubject); - - VerifyUnsubscribedFromBindingChangedEvent(testScope); - VerifySSESessionFactoryDisposedOnce(testScope); - } - - private static void VerifySSESessionFactoryDisposedOnce(TestScope testScope) - { - testScope.SSESessionFactoryMock.Verify(sessionFactory => sessionFactory.Dispose(), Times.Once); - } - - private static void VerifyUnsubscribedFromBindingChangedEvent(TestScope testScope) - { - testScope.ActiveSolutionBoundTrackerMock.VerifyRemove( - tracker => - tracker.SolutionBindingChanged -= - It.IsAny>(), - Times.Once); - } - - private static void CallDisposeMultipleTimes(SSESessionManager testSubject) - { - testSubject.Dispose(); - testSubject.Dispose(); - testSubject.Dispose(); - } - - private class TestScope - { - private readonly MockRepository mockRepository; - private readonly MockSequence callOrder = new(); - - public TestScope(BindingConfiguration initialBindingState = null) - { - mockRepository = new MockRepository(MockBehavior.Strict); - ActiveSolutionBoundTrackerMock = mockRepository.Create(); - SSESessionFactoryMock = mockRepository.Create(); - - // This is not in a sequence so we can call it multiple times - ActiveSolutionBoundTrackerMock - .SetupGet(tracker => tracker.CurrentConfiguration) - .Returns(initialBindingState ?? BindingConfiguration.Standalone); - } - - public Mock ActiveSolutionBoundTrackerMock { get; } - public Mock SSESessionFactoryMock { get; } - public OnSessionFailedAsync CapturedSessionFailedCallback { get; private set; } - - public SSESessionManager CreateTestSubject() - { - return new SSESessionManager( - ActiveSolutionBoundTrackerMock.Object, - SSESessionFactoryMock.Object, - Mock.Of()); - } - - public static BindingConfiguration CreateConnectedModeSonarQubeBindingConfiguration(string projectKey) - { - var sonarQube = new ServerConnection.SonarQube(new Uri("http://localhost")); - return CreateConnectedModeBindingConfiguration(projectKey, sonarQube); - } - - public static BindingConfiguration CreateConnectedModeSonarCloudBindingConfiguration(string projectKey) - { - var sonarCloud = new ServerConnection.SonarCloud(projectKey); - return CreateConnectedModeBindingConfiguration(projectKey, sonarCloud); - } - - private static BindingConfiguration CreateConnectedModeBindingConfiguration(string projectKey, ServerConnection serverConnection) - { - var randomString = Guid.NewGuid().ToString(); - var bindingConfiguration = new BindingConfiguration( - new BoundServerProject(randomString, projectKey, serverConnection), - SonarLintMode.Connected, - randomString); - return bindingConfiguration; - } - - public void SetUpCorrectDisposeOrder(Mock currentSession) - { - ActiveSolutionBoundTrackerMock.SetupRemove(tracker => - tracker.SolutionBindingChanged -= It.IsAny>()); - - SSESessionFactoryMock.InSequence(callOrder).Setup(sessionFactory => sessionFactory.Dispose()); - - currentSession?.InSequence(callOrder).Setup(session => session.Dispose()); - } - - public Mock SetUpSSEFactoryToReturnNoOpSSESession(string projectKey, Action factoryMockCallback = null) - { - var sseSessionMock = mockRepository.Create(); - - SSESessionFactoryMock.InSequence(callOrder) - .Setup(sessionFactory => sessionFactory.Create(projectKey, It.IsAny())) - .Returns(sseSessionMock.Object) - .Callback((string projectKey,OnSessionFailedAsync callbackAction) => { - CapturedSessionFailedCallback = callbackAction; - factoryMockCallback?.Invoke(); - }); - - sseSessionMock - .InSequence(callOrder) - .Setup(session => session.PumpAllAsync()) - .Returns(Task.CompletedTask); - - return sseSessionMock; - } - - public void RaiseInConnectedModeEvent(string projectKey) - { - var openProjectEvent = CreateConnectedModeSonarQubeBindingConfiguration(projectKey); - RaiseSolutionBindingEvent(openProjectEvent); - } - - public void RaiseInStandaloneModeEvent() - { - RaiseSolutionBindingEvent(BindingConfiguration.Standalone); - } - - private void RaiseSolutionBindingEvent(BindingConfiguration bindingConfiguration) - { - ActiveSolutionBoundTrackerMock.Raise(tracker => tracker.SolutionBindingChanged += null, new ActiveSolutionBindingEventArgs(bindingConfiguration)); - } - } -} diff --git a/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionTests.cs b/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionTests.cs deleted file mode 100644 index 55cc3dd44e..0000000000 --- a/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionTests.cs +++ /dev/null @@ -1,288 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client; -using SonarQube.Client.Models.ServerSentEvents; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Issue; -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.QualityProfile; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.ServerSentEvents; - -[TestClass] -public class SSESessionTests -{ - [TestMethod] - public void PumpAllAsync_WhenSonarQubeRefusesConnection_DoesNotThrow() - { - var testScope = new TestScope(); - testScope.SetUpSwitchToBackgroundThread(); - testScope.SonarQubeServiceMock - .InSequence(testScope.CallOrder) - .Setup(sqs => sqs.CreateSSEStreamReader(It.IsAny(), It.IsAny())) - .ReturnsAsync((ISSEStreamReader)null); - - Func act = () => testScope.TestSubject.PumpAllAsync(); - - act.Should().NotThrow(); - } - - - [TestMethod] - public async Task PumpAllAsync_SelectsPublisherCorrectlyAndPreservesOrderWithinType() - { - var testScope = new TestScope(); - var inputSequence = new IServerEvent[] - { - Mock.Of(), - Mock.Of(), - Mock.Of(), - Mock.Of(), - Mock.Of(), - }; - testScope.SetUpSwitchToBackgroundThread(); - var sseStreamMock = testScope.SetUpSQServiceToSuccessfullyReturnSSEStreamReader(); - testScope.SetUpSSEStreamReaderToReturnEventsSequenceAndExit(sseStreamMock, inputSequence); - - await testScope.TestSubject.PumpAllAsync(); - - CheckEventsSequence(testScope.IssuePublisherMock.Invocations); - CheckEventsSequence(testScope.QualityProfilePublisherMock.Invocations); - - // note: can't pass Mock> because Mock's generic type is not covariant and we use the publisher interface through inheritor interfaces - void CheckEventsSequence(IEnumerable publisherMockInvocations) where T : class, IServerEvent - { - var publishedEvents = publisherMockInvocations.Select(call => call.Arguments.First() as T); - var expectedPublishedEvents = inputSequence.Where(issuesEvent => issuesEvent is T); - - publishedEvents.Should().BeEquivalentTo(expectedPublishedEvents); - } - } - - [TestMethod] - public async Task PumpAllAsync_WhenNullEvent_Ignores() - { - var testScope = new TestScope(); - testScope.SetUpSwitchToBackgroundThread(); - var sseStreamMock = testScope.SetUpSQServiceToSuccessfullyReturnSSEStreamReader(); - testScope.SetUpSSEStreamReaderToReturnEventsSequenceAndExit(sseStreamMock, - new IServerEvent[] - { - Mock.Of(), - null, - Mock.Of(), - Mock.Of(), - Mock.Of() - }); - - await testScope.TestSubject.PumpAllAsync(); - - testScope.IssuePublisherMock.Verify(publisher => publisher.Publish(It.IsAny()), Times.Exactly(2)); - testScope.QualityProfilePublisherMock.Verify(publisher => publisher.Publish(It.IsAny()), Times.Exactly(2)); - } - - [TestMethod] - public async Task PumpAllAsync_WhenUnsupportedEvent_Ignores() - { - var testScope = new TestScope(); - testScope.SetUpSwitchToBackgroundThread(); - var sseStreamMock = testScope.SetUpSQServiceToSuccessfullyReturnSSEStreamReader(); - - testScope.SetUpSSEStreamReaderToReturnEventsSequenceAndExit(sseStreamMock, - new IServerEvent[] - { - Mock.Of(), - Mock.Of(), - Mock.Of() - }); - - await testScope.TestSubject.PumpAllAsync(); - - testScope.IssuePublisherMock.Verify(publisher => publisher.Publish(It.IsAny()), Times.Exactly(1)); - testScope.QualityProfilePublisherMock.Verify(publisher => publisher.Publish(It.IsAny()), Times.Exactly(1)); - } - - [TestMethod] - public async Task PumpAllAsync_WhenPublisherErrors_NonCriticalError_ErrorLoggedAndSessionIsDisposed() - { - var testScope = new TestScope(); - testScope.SetUpSwitchToBackgroundThread(); - var sseStreamMock = testScope.SetUpSQServiceToSuccessfullyReturnSSEStreamReader(); - sseStreamMock.Setup(x => x.ReadAsync()).Throws(new NotImplementedException("this is a test")); - - testScope.LoggerMock.Setup(x => x.LogVerbose(It.Is(s => s.Contains("this is a test")), Array.Empty())); - - await testScope.TestSubject.PumpAllAsync(); - - testScope.LoggerMock.Verify(x=> x.LogVerbose(It.Is(s=> s.Contains("this is a test")), Array.Empty()), Times.Once); - testScope.CapturedSessionToken.Value.IsCancellationRequested.Should().BeTrue(); - } - - [TestMethod] - public void PumpAllAsync_WhenPublisherErrors_CriticalError_SessionThrows() - { - var testScope = new TestScope(); - testScope.SetUpSwitchToBackgroundThread(); - var sseStreamMock = testScope.SetUpSQServiceToSuccessfullyReturnSSEStreamReader(); - sseStreamMock.Setup(x => x.ReadAsync()).Throws(new DivideByZeroException("this is a test")); - - Func func = async () => await testScope.TestSubject.PumpAllAsync(); - - func.Should().ThrowExactly().And.Message.Should().Be("this is a test"); - testScope.LoggerMock.Invocations.Count.Should().Be(0); - testScope.CapturedSessionToken.Value.IsCancellationRequested.Should().BeFalse(); - } - - [TestMethod] - public void PumpAllAsync_AfterDisposed_Throws() - { - var testScope = new TestScope(); - testScope.TestSubject.Dispose(); - - var act = () => testScope.TestSubject.PumpAllAsync(); - - act.Should().Throw(); - } - - [TestMethod] - public async Task Dispose_FinishesSession() - { - var testScope = new TestScope(); - testScope.SetUpSwitchToBackgroundThread(); - var sseStreamMock = testScope.SetUpSQServiceToSuccessfullyReturnSSEStreamReader(); - var readTcs = new TaskCompletionSource(); - sseStreamMock.Setup(x => x.ReadAsync()).Returns(readTcs.Task); - - var pumpTask = testScope.TestSubject.PumpAllAsync(); - testScope.TestSubject.Dispose(); - readTcs.SetResult(null); - await pumpTask; - - var sessionToken = testScope.CapturedSessionToken; - sessionToken.Should().NotBeNull(); - sessionToken.Value.IsCancellationRequested.Should().BeTrue(); - } - - [TestMethod] - public async Task NonCriticalException_InvokesOnSessionFailedAsync() - { - var testScope = new TestScope(); - testScope.SetUpSwitchToBackgroundThread(); - - var sseStreamMock = testScope.SetUpSQServiceToSuccessfullyReturnSSEStreamReader(); - sseStreamMock.Setup(x => x.ReadAsync()).Throws(new NotImplementedException("this is a test")); - - await testScope.TestSubject.PumpAllAsync(); - - testScope.OnSessionFailedAsyncMock.Invocations.Should().HaveCount(1); - } - - public interface IDummyServerEvent : IServerEvent { } - - private static Mock GetOnSessionFailedAsyncMock() - { - var createOp = new Mock(); - createOp.Setup(x => x.Invoke(It.IsAny())); - return createOp; - } - - private class TestScope - { - private readonly MockRepository mockRepository; - - public TestScope() - { - mockRepository = new MockRepository(MockBehavior.Strict); - SonarQubeServiceMock = mockRepository.Create(); - IssuePublisherMock = mockRepository.Create(MockBehavior.Loose); - QualityProfilePublisherMock = mockRepository.Create(MockBehavior.Loose); - ThreadHandlingMock = mockRepository.Create(); - LoggerMock = new Mock(); - OnSessionFailedAsyncMock = GetOnSessionFailedAsyncMock(); - - var factory = new SSESessionFactory( - SonarQubeServiceMock.Object, - IssuePublisherMock.Object, - QualityProfilePublisherMock.Object, - ThreadHandlingMock.Object, - LoggerMock.Object); - - TestSubject = factory.Create("blalala", OnSessionFailedAsyncMock.Object); - } - - private Mock ThreadHandlingMock { get; } - public Mock SonarQubeServiceMock { get; } - public Mock IssuePublisherMock { get; } - public Mock QualityProfilePublisherMock { get; } - public Mock LoggerMock { get; } - public CancellationToken? CapturedSessionToken { get; private set; } - public MockSequence CallOrder { get; } = new MockSequence(); - public ISSESession TestSubject { get; } - public Mock OnSessionFailedAsyncMock { get; } - - public void SetUpSSEStreamReaderToReturnEventsSequenceAndExit(Mock sseStreamMock, IServerEvent[] inputSequence) - { - foreach (var serverEvent in inputSequence) - { - sseStreamMock - .InSequence(CallOrder) - .Setup(r => r.ReadAsync()) - .ReturnsAsync(serverEvent); - } - - sseStreamMock - .InSequence(CallOrder) - .Setup(r => r.ReadAsync()) - .ReturnsAsync(() => - { - TestSubject.Dispose(); - return null; - }); - } - - public Mock SetUpSQServiceToSuccessfullyReturnSSEStreamReader() - { - var sseStreamMock = mockRepository.Create(); - - SonarQubeServiceMock - .InSequence(CallOrder) - .Setup(client => client.CreateSSEStreamReader(It.IsAny(), - It.Is(token => token != CancellationToken.None))) - .ReturnsAsync((string _, CancellationToken tokenArg) => - { - CapturedSessionToken = tokenArg; - return sseStreamMock.Object; - }); - - return sseStreamMock; - } - - public void SetUpSwitchToBackgroundThread() - { - ThreadHandlingMock - .InSequence(CallOrder) - .Setup(th => th.SwitchToBackgroundThread()) - .Returns(new NoOpThreadHandler.NoOpAwaitable()); - } - } -} diff --git a/src/ConnectedMode.UnitTests/StatefulServerBranchProviderTests.cs b/src/ConnectedMode.UnitTests/StatefulServerBranchProviderTests.cs index 3d065a679e..a6028a3a4d 100644 --- a/src/ConnectedMode.UnitTests/StatefulServerBranchProviderTests.cs +++ b/src/ConnectedMode.UnitTests/StatefulServerBranchProviderTests.cs @@ -19,268 +19,215 @@ */ using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; using SonarLint.VisualStudio.Core.ConfigurationScope; -using SonarLint.VisualStudio.Core.Synchronization; +using SonarLint.VisualStudio.Core.Initialization; using SonarLint.VisualStudio.SLCore; using SonarLint.VisualStudio.SLCore.Core; +using SonarLint.VisualStudio.SLCore.Listener.Branch; using SonarLint.VisualStudio.SLCore.Service.Branch; +using SonarLint.VisualStudio.SLCore.State; using SonarLint.VisualStudio.TestInfrastructure; -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests; + +[TestClass] +public class SLCoreGitChangeNotifierTests { - [TestClass] - public class StatefulServerBranchProviderTests + private NoOpThreadHandler threadHandling; + private ISLCoreServiceProvider slCoreServiceProvider; + private TestLogger logger; + private SlCoreGitChangeNotifier testSubject; + private IActiveConfigScopeTracker activeConfigScopeTracker; + private ISonarProjectBranchSlCoreService sonarProjectBranchSlCoreService; + private IBoundSolutionGitMonitor gitMonitor; + private IInitializationProcessorFactory initializationProcessorFactory; + + [TestInitialize] + public void TestInitialize() { - private readonly ActiveSolutionBindingEventArgs connectedModeBinding = new(new BindingConfiguration(default, SonarLintMode.Connected, default)); - private readonly ActiveSolutionBindingEventArgs standaloneModeBinding = new(BindingConfiguration.Standalone); - private IThreadHandling threadHandling; - private IActiveSolutionBoundTracker activeSolutionBoundTracker; - private ISLCoreServiceProvider slCoreServiceProvider; - private TestLogger logger; - private IAsyncLockFactory asyncLockFactory; - private StatefulServerBranchProvider testSubject; - private IServerBranchProvider serverBranchProvider; - private IActiveConfigScopeTracker activeConfigScopeTracker; - private ISonarProjectBranchSlCoreService sonarProjectBranchSlCoreService; - private IAsyncLock asyncLock; - - [TestInitialize] - public void TestInitialize() - { - serverBranchProvider = Substitute.For(); - activeSolutionBoundTracker = Substitute.For(); - activeConfigScopeTracker = Substitute.For(); - logger = new TestLogger(); - threadHandling = new NoOpThreadHandler(); - slCoreServiceProvider = Substitute.For(); - MockSonarProjectBranchSlCoreService(); - MockAsyncLock(); - testSubject = new StatefulServerBranchProvider(serverBranchProvider, activeSolutionBoundTracker, activeConfigScopeTracker, slCoreServiceProvider, logger, threadHandling, asyncLockFactory); - } - - [TestMethod] - public void MefCtor_CheckIsExported() => - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - - [TestMethod] - public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); - - [TestMethod] - public async Task GetServerBranchNameAsync_WhenCalled_UsesCache() - { - MockGetServerBranchName("Branch"); - - // First call: Should use IServerBranchProvider - var serverBranch = await testSubject.GetServerBranchNameAsync(CancellationToken.None); - serverBranch.Should().Be("Branch"); - serverBranchProvider.VerifyGetServerBranchNameCalled(1); - - // Second call: Should use cache - serverBranchProvider.SetBranchNameToReturn("branch name that should not be returned - should use cached value"); - serverBranch = await testSubject.GetServerBranchNameAsync(CancellationToken.None); + activeConfigScopeTracker = Substitute.For(); + logger = Substitute.ForPartsOf(); + threadHandling = Substitute.ForPartsOf(); + slCoreServiceProvider = Substitute.For(); + gitMonitor = Substitute.For(); - // Not expecting any more threading calls since using the cache - serverBranch.Should().Be("Branch"); - serverBranchProvider.VerifyGetServerBranchNameCalled(1); - } - - [TestMethod] - public async Task GetServerBranchNameAsync_RunsOnBackgroundThread() - { - MockGetServerBranchName("Branch"); - var mockedThreadHandling = MockThreadHandling(); - testSubject = new StatefulServerBranchProvider(serverBranchProvider, activeSolutionBoundTracker, activeConfigScopeTracker, slCoreServiceProvider, logger, mockedThreadHandling, - asyncLockFactory); + MockSonarProjectBranchSlCoreService(); + } - var serverBranch = await testSubject.GetServerBranchNameAsync(CancellationToken.None); + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public void WhenInitialized_EventHandlersAreRegistered() + { + CreateAndInitializeTestSubject(); - await mockedThreadHandling.Received(1).RunOnBackgroundThread(Arg.Any>>()); - mockedThreadHandling.ReceivedCalls().Should().HaveCount(1); - serverBranch.Should().Be("Branch"); - serverBranchProvider.VerifyGetServerBranchNameCalled(1); - } + var initializationDependencies = new IRequireInitialization[] { gitMonitor }; + initializationProcessorFactory.Received(1).Create(Arg.Is>(x => x.SequenceEqual(initializationDependencies)), Arg.Any>()); - [TestMethod] - public async Task GetServerBranchNameAsync_WhenCalled_UpdatesCacheThreadSafe() + Received.InOrder(() => { - MockGetServerBranchName("Branch"); - - var serverBranch = await testSubject.GetServerBranchNameAsync(CancellationToken.None); + threadHandling.RunOnUIThreadAsync(Arg.Any()); + gitMonitor.HeadChanged += Arg.Any(); + activeConfigScopeTracker.CurrentConfigurationScopeChanged += Arg.Any>(); + }); + } - serverBranch.Should().Be("Branch"); - await asyncLock.Received(1).AcquireAsync(); - } + [TestMethod] + public void OnHeadChanged_NotifiesSlCoreAboutBranchChange() + { + CreateAndInitializeTestSubject(); - [DataTestMethod] - [DataRow(true)] - [DataRow(false)] - public async Task GetServerBranchNameAsync_PreSolutionBindingChanged_CacheIsCleared(bool isConnected) => - await TestEffectOfRaisingEventOnCacheAsync(RaisePreSolutionBindingUpdated, eventArg: isConnected ? connectedModeBinding : standaloneModeBinding, shouldClearCache: true); + const string expectedConfigScopeId = "expected-id"; + var configScope = new Core.ConfigurationScope.ConfigurationScope(expectedConfigScopeId); + activeConfigScopeTracker.Current.Returns(configScope); + MockSonarProjectBranchSlCoreService(); - [TestMethod] - public async Task GetServerBranchNameAsync_PreSolutionBindingUpdated_CacheIsCleared() => await TestEffectOfRaisingEventOnCacheAsync(RaisePreSolutionBindingUpdated, shouldClearCache: true); + gitMonitor.HeadChanged += Raise.EventWith(gitMonitor, EventArgs.Empty); - [TestMethod] - public async Task GetServerBranchNameAsync_SolutionBindingChanged_CacheIsNotCleared() => await TestEffectOfRaisingEventOnCacheAsync(RaiseSolutionBindingUpdated, shouldClearCache: false); + sonarProjectBranchSlCoreService.Received(1) + .DidVcsRepositoryChange(Arg.Is(p => p.configurationScopeId == expectedConfigScopeId)); + } - [TestMethod] - public async Task GetServerBranchNameAsync_SolutionBindingUpdated_CacheIsNotCleared() => await TestEffectOfRaisingEventOnCacheAsync(RaiseSolutionBindingUpdated, shouldClearCache: false); + [TestMethod] + public void OnHeadChanged_WhenConfigScopeIsNull_DoesNotNotifySlCore() + { + CreateAndInitializeTestSubject(); - [TestMethod] - public void NotifySlCoreBranchChange_BindingChanged_Connected_CallsDidVcsRepositoryChangeWithCorrectId() - { - const string expectedConfigScopeId = "expected-id"; - activeConfigScopeTracker.Current.Returns(new Core.ConfigurationScope.ConfigurationScope(expectedConfigScopeId)); - MockSonarProjectBranchSlCoreService(); - MockGetServerBranchName("OriginalBranch"); + activeConfigScopeTracker.Current.Returns((Core.ConfigurationScope.ConfigurationScope)null); + MockSonarProjectBranchSlCoreService(); - RaisePreSolutionBindingUpdated(connectedModeBinding); + gitMonitor.HeadChanged += Raise.EventWith(gitMonitor, EventArgs.Empty); - sonarProjectBranchSlCoreService.Received(1) - .DidVcsRepositoryChange(Arg.Is(p => p.configurationScopeId == expectedConfigScopeId)); - } + sonarProjectBranchSlCoreService.DidNotReceive() + .DidVcsRepositoryChange(Arg.Any()); + } - [TestMethod] - public void NotifySlCoreBranchChange_BindingChanged_Standalone_Ignores() - { - MockGetServerBranchName("OriginalBranch"); + [TestMethod] + public void OnHeadChanged_WhenServiceProviderFails_LogsError() + { + CreateAndInitializeTestSubject(); - RaisePreSolutionBindingUpdated(standaloneModeBinding); + const string expectedConfigScopeId = "expected-id"; + var configScope = new Core.ConfigurationScope.ConfigurationScope(expectedConfigScopeId); + activeConfigScopeTracker.Current.Returns(configScope); + MockSonarProjectBranchSlCoreService(wasFound: false); - activeConfigScopeTracker.ReceivedCalls().Should().HaveCount(1); // no other calls - slCoreServiceProvider.ReceivedCalls().Should().HaveCount(1); // no other calls - } + gitMonitor.HeadChanged += Raise.EventWith(gitMonitor, EventArgs.Empty); - [TestMethod] - public void NotifySlCoreBranchChange_BindingUpdated_CallsDidVcsRepositoryChangeWithCorrectId() - { - const string expectedConfigScopeId = "expected-id"; - activeConfigScopeTracker.Current.Returns(new Core.ConfigurationScope.ConfigurationScope(expectedConfigScopeId)); - MockSonarProjectBranchSlCoreService(); - MockGetServerBranchName("OriginalBranch"); + logger.AssertPartialOutputStringExists(SLCoreStrings.ServiceProviderNotInitialized); + sonarProjectBranchSlCoreService.DidNotReceive() + .DidVcsRepositoryChange(Arg.Any()); + } - RaisePreSolutionBindingUpdated(null); + [TestMethod] + public void OnConfigScopeChanged_WhenDefinitionChanges_RefreshesGitMonitor() + { + CreateAndInitializeTestSubject(); - sonarProjectBranchSlCoreService.Received(1).DidVcsRepositoryChange(Arg.Is(p => p.configurationScopeId == expectedConfigScopeId)); - } + var args = new ConfigurationScopeChangedEventArgs(true); + activeConfigScopeTracker.CurrentConfigurationScopeChanged += Raise.EventWith(activeConfigScopeTracker, args); - [TestMethod] - public void NotifySlCoreBranchChange_BindingChanged_WhenServiceProviderReturnsFalse_LogsError() - { - MockSonarProjectBranchSlCoreService(wasFound: false); - MockGetServerBranchName("OriginalBranch"); + gitMonitor.Received(1).Refresh(); + } - RaisePreSolutionBindingUpdated(connectedModeBinding); + [TestMethod] + public void OnConfigScopeChanged_WhenDefinitionDoesNotChange_DoesNotRefreshGitMonitor() + { + CreateAndInitializeTestSubject(); - logger.AssertPartialOutputStringExists(SLCoreStrings.ServiceProviderNotInitialized); - slCoreServiceProvider.ReceivedCalls().Should().HaveCount(1); // no other calls - } + var args = new ConfigurationScopeChangedEventArgs(false); + activeConfigScopeTracker.CurrentConfigurationScopeChanged += Raise.EventWith(activeConfigScopeTracker, args); - [TestMethod] - public void NotifySlCoreBranchChange_BindingUpdated_WhenServiceProviderReturnsFalse_LogsError() - { - MockSonarProjectBranchSlCoreService(wasFound: false); - MockGetServerBranchName("OriginalBranch"); + gitMonitor.DidNotReceive().Refresh(); + } - RaisePreSolutionBindingUpdated(null); + [TestMethod] + public void Dispose_WhenInitialized_UnhooksEventHandlers() + { + CreateAndInitializeTestSubject(); - logger.AssertPartialOutputStringExists(SLCoreStrings.ServiceProviderNotInitialized); - slCoreServiceProvider.ReceivedCalls().Should().HaveCount(1); // no other calls - } + testSubject.Dispose(); + testSubject.Dispose(); + testSubject.Dispose(); - [TestMethod] - public void Dispose_UnhooksEventHandlers() - { - testSubject.Dispose(); + gitMonitor.ReceivedWithAnyArgs(1).HeadChanged -= default; + activeConfigScopeTracker.ReceivedWithAnyArgs(1).CurrentConfigurationScopeChanged -= default; + } - // Should only unhook the "Pre-" event handlers - activeSolutionBoundTracker.Received(1).PreSolutionBindingChanged -= Arg.Any>(); - activeSolutionBoundTracker.Received(1).PreSolutionBindingUpdated -= Arg.Any(); + [TestMethod] + public void Dispose_WhenNotInitialized_DoesNotExecute() + { + CreateUninitializedTestSubject(out var barrier); - activeSolutionBoundTracker.DidNotReceive().SolutionBindingChanged -= Arg.Any>(); - activeSolutionBoundTracker.DidNotReceive().SolutionBindingUpdated -= Arg.Any(); - } + CheckDisposed(); - private void MockGetServerBranchName(string branchName) => serverBranchProvider.GetServerBranchNameAsync(Arg.Any()).Returns(branchName); + barrier.SetResult(1); + testSubject.InitializationProcessor.InitializeAsync().GetAwaiter().GetResult(); + testSubject.InitializationProcessor.IsFinalized.Should().BeTrue(); + CheckDisposed(); - private void MockAsyncLock() + void CheckDisposed() { - asyncLock = Substitute.For(); - asyncLockFactory = Substitute.For(); - asyncLockFactory.Create().Returns(asyncLock); - } + var act = () => testSubject.Dispose(); - private async Task TestEffectOfRaisingEventOnCacheAsync( - Action eventAction, - bool shouldClearCache, - EventArgs eventArg = null) - { - MockGetServerBranchName("OriginalBranch"); - - //first call: Should use IServerBranchProvider - var serverBranch = await testSubject.GetServerBranchNameAsync(CancellationToken.None); - - serverBranch.Should().Be("OriginalBranch"); - serverBranchProvider.VerifyGetServerBranchNameCalled(1); - - serverBranchProvider.SetBranchNameToReturn("NewBranch"); - - // Raise event - should *not* trigger clearing the cache - eventAction(eventArg); - - //second call: may or may not use the cache - serverBranch = await testSubject.GetServerBranchNameAsync(CancellationToken.None); - - if (shouldClearCache) - { - serverBranch.Should().Be("NewBranch"); - serverBranchProvider.VerifyGetServerBranchNameCalled(2); - asyncLock.Received(1).Acquire(); - } - else - { - serverBranch.Should().Be("OriginalBranch"); - serverBranchProvider.VerifyGetServerBranchNameCalled(1); - asyncLock.DidNotReceive().Acquire(); - } + act.Should().NotThrow(); + gitMonitor.DidNotReceiveWithAnyArgs().HeadChanged += default; + gitMonitor.DidNotReceiveWithAnyArgs().HeadChanged -= default; + activeConfigScopeTracker.DidNotReceiveWithAnyArgs().CurrentConfigurationScopeChanged += default; + activeConfigScopeTracker.DidNotReceiveWithAnyArgs().CurrentConfigurationScopeChanged -= default; } + } - private void RaisePreSolutionBindingUpdated(EventArgs eventArg) => activeSolutionBoundTracker.PreSolutionBindingUpdated += Raise.EventWith(null, eventArg); - - private void RaiseSolutionBindingUpdated(EventArgs eventArg) => activeSolutionBoundTracker.SolutionBindingUpdated += Raise.EventWith(null, eventArg); - - private void MockSonarProjectBranchSlCoreService(bool wasFound = true) + private void MockSonarProjectBranchSlCoreService(bool wasFound = true) + { + sonarProjectBranchSlCoreService = Substitute.For(); + slCoreServiceProvider.TryGetTransientService(out ISonarProjectBranchSlCoreService _).Returns(x => { - sonarProjectBranchSlCoreService = Substitute.For(); - slCoreServiceProvider.TryGetTransientService(out ISonarProjectBranchSlCoreService _).Returns(x => - { - x[0] = sonarProjectBranchSlCoreService; - return wasFound; - }); - } + x[0] = sonarProjectBranchSlCoreService; + return wasFound; + }); + } - private IThreadHandling MockThreadHandling() - { - var mockedThreadHandling = Substitute.For(); - mockedThreadHandling.RunOnBackgroundThread(Arg.Any>>()).Returns(info => info.Arg>>()()); - return mockedThreadHandling; - } + private void CreateUninitializedTestSubject(out TaskCompletionSource barrier) + { + var tcs = barrier = new TaskCompletionSource(); + initializationProcessorFactory = MockableInitializationProcessor.CreateFactory( + threadHandling, + logger, + processor => MockableInitializationProcessor.ConfigureWithWait(processor, tcs)); + + testSubject = new SlCoreGitChangeNotifier( + activeConfigScopeTracker, + slCoreServiceProvider, + gitMonitor, + logger, + threadHandling, + initializationProcessorFactory); } - internal static class StatefulServerBranchProviderTestsExtensions + private void CreateAndInitializeTestSubject() { - public static void VerifyGetServerBranchNameCalled(this IServerBranchProvider serverBranchProvider, int times) => - serverBranchProvider.Received(times).GetServerBranchNameAsync(Arg.Any()); + initializationProcessorFactory = MockableInitializationProcessor.CreateFactory(threadHandling, logger); + + testSubject = new SlCoreGitChangeNotifier( + activeConfigScopeTracker, + slCoreServiceProvider, + gitMonitor, + logger, + threadHandling, + initializationProcessorFactory); - public static void SetBranchNameToReturn(this IServerBranchProvider serverBranchProvider, string branchName) => - serverBranchProvider.GetServerBranchNameAsync(Arg.Any()).Returns(branchName); + testSubject.InitializationProcessor.InitializeAsync().GetAwaiter().GetResult(); } } diff --git a/src/ConnectedMode.UnitTests/Suppressions/RoslynSuppressionUpdaterTests.cs b/src/ConnectedMode.UnitTests/Suppressions/RoslynSuppressionUpdaterTests.cs deleted file mode 100644 index e1d5e5cb41..0000000000 --- a/src/ConnectedMode.UnitTests/Suppressions/RoslynSuppressionUpdaterTests.cs +++ /dev/null @@ -1,391 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using Microsoft.VisualStudio.Threading; -using SonarLint.VisualStudio.ConnectedMode.Helpers; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Infrastructure.VS; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Suppressions; - -[TestClass] -public class RoslynSuppressionUpdaterTests -{ - private ICancellableActionRunner actionRunner; - private ILogger logger; - private IServerQueryInfoProvider queryInfo; - private ISonarQubeService server; - private RoslynSuppressionUpdater testSubject; - private IThreadHandling threadHandling; - private readonly EventHandler suppressedIssuesReloaded = Substitute.For>(); - private readonly EventHandler newIssuesSuppressed = Substitute.For>(); - private readonly EventHandler suppressionsRemoved = Substitute.For>(); - - [TestInitialize] - public void TestInitialize() - { - server = Substitute.For(); - queryInfo = Substitute.For(); - logger = Substitute.For(); - logger.ForContext(Arg.Any()).Returns(logger); - threadHandling = new NoOpThreadHandler(); - actionRunner = new SynchronizedCancellableActionRunner(logger); - testSubject = CreateTestSubject(actionRunner, threadHandling); - testSubject.SuppressedIssuesReloaded += suppressedIssuesReloaded; - testSubject.NewIssuesSuppressed += newIssuesSuppressed; - testSubject.SuppressionsRemoved += suppressionsRemoved; - } - - [TestMethod] - public void MefCtor_CheckIsExported() => - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - - [TestMethod] - public void Ctor_SetsLoggerContext() => logger.Received(1).ForContext(Resources.ConnectedModeLogContext, Resources.RoslynSuppressionsLogContext); - - [TestMethod] - [DataRow(null, null)] - [DataRow(null, "branch")] - [DataRow("projectKey", null)] - public async Task UpdateAll_MissingServerQueryInfo_DoesNotInvokeEvent(string projectKey, string branchName) - { - // Server query info is not available -> give up - MockQueryInfoProvider(projectKey, branchName); - - await testSubject.UpdateAllServerSuppressionsAsync(); - - await queryInfo.Received(1).GetProjectKeyAndBranchAsync(Arg.Any()); - server.ReceivedCalls().Should().HaveCount(0); - VerifySuppressedIssuesReloadedNotInvoked(); - } - - [TestMethod] - public async Task UpdateAll_HasServerQueryInfo_ServerQueriedAndEventInvoked() - { - // Happy path - fetch and update - MockQueryInfoProvider("project", "branch"); - var issue = CreateIssue("issue1"); - MockSonarQubeService("project", "branch", null, issue); - - await testSubject.UpdateAllServerSuppressionsAsync(); - - await queryInfo.Received(1).GetProjectKeyAndBranchAsync(Arg.Any()); - await server.Received(1).GetSuppressedRoslynIssuesAsync("project", "branch", null, Arg.Any()); - server.ReceivedCalls().Should().HaveCount(1); - VerifySuppressedIssuesReloadedInvoked(expectedAllSuppressedIssues: [issue]); - } - - [TestMethod] - public async Task UpdateAll_RunOnBackgroundThreadInActionRunner() - { - var mockedActionRunner = CreateMockedActionRunner(); - var mockedThreadHandling = CreateMockedThreadHandling(); - var mockedTestSubject = CreateTestSubject(mockedActionRunner, mockedThreadHandling); - - await mockedTestSubject.UpdateAllServerSuppressionsAsync(); - - Received.InOrder(() => - { - mockedThreadHandling.RunOnBackgroundThread(Arg.Any>>>()); - mockedActionRunner.RunAsync(Arg.Any>()); - queryInfo.GetProjectKeyAndBranchAsync(Arg.Any()); - }); - queryInfo.ReceivedCalls().Should().HaveCount(1); // no other calls - } - - [TestMethod] - public void UpdateAll_CriticalExpression_NotHandled() - { - queryInfo.When(x => x.GetProjectKeyAndBranchAsync(Arg.Any())).Throw(new StackOverflowException("thrown in a test")); - - var operation = testSubject.UpdateAllServerSuppressionsAsync; - - operation.Should().Throw().And.Message.Should().Be("thrown in a test"); - AssertMessageArgsDoesNotExist("thrown in a test"); - } - - [TestMethod] - public void UpdateAll_NonCriticalExpression_IsSuppressed() - { - queryInfo.When(x => x.GetProjectKeyAndBranchAsync(Arg.Any())).Throw(new InvalidOperationException("thrown in a test")); - - var operation = testSubject.UpdateAllServerSuppressionsAsync; - - operation.Should().NotThrow(); - AssertMessageArgsExists("thrown in a test"); - } - - [TestMethod] - public void UpdateAll_OperationCancelledException_CancellationMessageLogged() - { - queryInfo.When(x => x.GetProjectKeyAndBranchAsync(Arg.Any())).Throw(new OperationCanceledException("thrown in a test")); - - var operation = testSubject.UpdateAllServerSuppressionsAsync; - - operation.Should().NotThrow(); - AssertMessageArgsDoesNotExist("thrown in a test"); - AssertMessageExists(Resources.Suppressions_FetchOperationCancelled); - } - - [TestMethod] - [Ignore] // Flaky. See https://github.com/SonarSource/sonarlint-visualstudio/issues/4307 - public async Task UpdateAll_CallInProgress_CallIsCancelled() - { - var testTimeout = GetThreadedTestTimeout(); - - var signalToBlockFirstCall = new ManualResetEvent(false); - var signalToBlockSecondCall = new ManualResetEvent(false); - - var isFirstCall = true; - - queryInfo.GetProjectKeyAndBranchAsync(Arg.Any()) - .Returns(("projectKey", "branch")); - queryInfo.When(x => x.GetProjectKeyAndBranchAsync(Arg.Any())) - .Do(x => - { - if (isFirstCall) - { - Log("[1] In first call"); - isFirstCall = false; - - Log("[1] Unblocking second call..."); - signalToBlockSecondCall.Set(); - - Log("[1] Waiting to be unblocked..."); - signalToBlockFirstCall.WaitOne(testTimeout); - - Log("[1] First call finished"); - } - else - { - Log("[2] In second call"); - - Log("[2] Unblocking the first call..."); - signalToBlockFirstCall.Set(); - Log("[2] Second call finished"); - } - }); - - var mockedTestSubject = CreateTestSubject( - // Note: need a real thread-handling implementation here as the test - // needs multiple threads. - actionRunner, ThreadHandling.Instance); - - // 1. First call - don't wait for it to finish, since it should be blocked - mockedTestSubject.UpdateAllServerSuppressionsAsync().Forget(); - - Log("[main test] Waiting to be unblocked..."); - signalToBlockSecondCall.WaitOne(testTimeout) - .Should().BeTrue(); // not expecting the test to timeout - - // 2. Second call - "await" means we're waiting for it to run to completion - await mockedTestSubject.UpdateAllServerSuppressionsAsync(); - - queryInfo.ReceivedCalls().Should().HaveCount(2); - var call1Token = (CancellationToken)queryInfo.ReceivedCalls().ToList()[0].GetArguments()[0]; - var call2Token = (CancellationToken)queryInfo.ReceivedCalls().ToList()[1].GetArguments()[0]; - - call1Token.IsCancellationRequested.Should().BeTrue(); - call2Token.IsCancellationRequested.Should().BeFalse(); - - AssertMessageExists(Resources.Suppressions_FetchOperationCancelled); - - // If cancellation worked then we should only have made one call to the SonarQubeService - server.ReceivedCalls().Should().HaveCount(1); - VerifySuppressedIssuesReloadedInvoked([]); - - static void Log(string message) => Console.WriteLine($"[Thread {Thread.CurrentThread.ManagedThreadId}] {message}"); - } - - [TestMethod] - public async Task UpdateSuppressedIssuesAsync_EmptyIssues_IssuesNotResolved_SuppressionsRemovedEventNotInvokedAndNoServerCalls() - { - await testSubject.UpdateSuppressedIssuesAsync(isResolved: false, [], CancellationToken.None); - - queryInfo.ReceivedCalls().Count().Should().Be(0); - server.ReceivedCalls().Count().Should().Be(0); - VerifySuppressionsRemovedNotInvoked(); - } - - [TestMethod] - public async Task UpdateSuppressedIssuesAsync_IssuesNotResolved_IssuesFetched() - { - string[] issueKeys = ["issue1", "issue2"]; - - await testSubject.UpdateSuppressedIssuesAsync(isResolved: false, issueKeys, CancellationToken.None); - - VerifySuppressionsRemovedInvoked(issueKeys); - } - - [TestMethod] - public async Task UpdateSuppressedIssuesAsync_EmptyIssues_NewIssuesSuppressedNotInvokedAndNoServerCalls() - { - MockQueryInfoProvider("proj1", "branch1"); - - await testSubject.UpdateSuppressedIssuesAsync(isResolved: true, [], CancellationToken.None); - - queryInfo.ReceivedCalls().Count().Should().Be(0); - server.ReceivedCalls().Count().Should().Be(0); - VerifyNewIssuesSuppressedNotInvoked(); - } - - [TestMethod] - public async Task UpdateSuppressedIssuesAsync_IssuesSuppressed_IssuesFetched() - { - MockQueryInfoProvider("proj1", "branch1"); - var expectedFetchedIssues = new[] { CreateIssue("issue1") }; - MockSonarQubeService( - "proj1", - "branch1", - ["issue1"], - expectedFetchedIssues); - - await testSubject.UpdateSuppressedIssuesAsync(isResolved: true, ["issue1"], CancellationToken.None); - - await server.Received(1).GetSuppressedRoslynIssuesAsync( - "proj1", - "branch1", - Arg.Is(x => x.SequenceEqual(new[] { "issue1" })), - Arg.Any()); - VerifyNewIssuesSuppressedInvoked(expectedFetchedIssues); - AssertMessageExists(Resources.Suppressions_Fetch_Issues); - AssertMessageExists(Resources.Suppression_Fetch_Issues_Finished); - } - - [TestMethod] - public void UpdateSuppressedIssuesAsync_IssuesSuppressed_CriticalExpression_NotHandled() - { - MockQueryInfoProvider("proj1", "branch1"); - queryInfo.When(x => x.GetProjectKeyAndBranchAsync(Arg.Any())).Throw(new StackOverflowException("thrown in a test")); - - var operation = () => testSubject.UpdateSuppressedIssuesAsync(isResolved: true, ["issue1"], CancellationToken.None); - - operation.Should().Throw().And.Message.Should().Be("thrown in a test"); - AssertMessageArgsDoesNotExist("thrown in a test"); - VerifyNewIssuesSuppressedNotInvoked(); - } - - [TestMethod] - public void UpdateSuppressedIssuesAsync_IssuesSuppressed_NonCriticalExpression_IsSuppressed() - { - MockQueryInfoProvider("proj1", "branch1"); - queryInfo.When(x => x.GetProjectKeyAndBranchAsync(Arg.Any())).Throw(new InvalidOperationException("thrown in a test")); - - var operation = () => testSubject.UpdateSuppressedIssuesAsync(isResolved: true, ["issue1"], CancellationToken.None); - - operation.Should().NotThrow(); - AssertMessageArgsExists("thrown in a test"); - VerifyNewIssuesSuppressedNotInvoked(); - } - - [TestMethod] - public void UpdateSuppressedIssuesAsync_IssuesSuppressed_OperationCancelledException_CancellationMessageLogged() - { - MockQueryInfoProvider("proj1", "branch1"); - using var cancellationTokenSource = new CancellationTokenSource(); - queryInfo.When(x => x.GetProjectKeyAndBranchAsync(Arg.Any())).Do(_ => cancellationTokenSource.Cancel()); - - var operation = () => testSubject.UpdateSuppressedIssuesAsync(isResolved: true, ["issue1"], cancellationTokenSource.Token); - - operation.Should().NotThrow(); - AssertMessageExists(Resources.Suppressions_FetchOperationCancelled); - VerifyNewIssuesSuppressedNotInvoked(); - } - - private RoslynSuppressionUpdater CreateTestSubject(ICancellableActionRunner mockedActionRunner, IThreadHandling mockedThreadHandling) => - new(server, queryInfo, mockedActionRunner, logger, mockedThreadHandling); - - private void MockQueryInfoProvider(string projectKey, string branchName) => queryInfo.GetProjectKeyAndBranchAsync(Arg.Any()).Returns((projectKey, branchName)); - - private void MockSonarQubeService( - string projectKey, - string branchName, - string[] issueKeys, - params SonarQubeIssue[] issuesToReturn) => - server.GetSuppressedRoslynIssuesAsync(projectKey, branchName, Arg.Is(x => issueKeys == null || x.SequenceEqual(issueKeys)), Arg.Any()).Returns(issuesToReturn); - - private static SonarQubeIssue CreateIssue(string issueKey) => - new(issueKey, - null, null, null, null, null, true, - SonarQubeIssueSeverity.Info, DateTimeOffset.MinValue, - DateTimeOffset.MaxValue, null, null); - - private static TimeSpan GetThreadedTestTimeout() - // This test uses a number of manual signals to control the order of execution. - // We want a longer timeout when debugging. - => - Debugger.IsAttached ? TimeSpan.FromMinutes(2) : TimeSpan.FromMilliseconds(200); - - private static IThreadHandling CreateMockedThreadHandling() - { - var mockedThreadHandling = Substitute.For(); - mockedThreadHandling.When(x => x.RunOnBackgroundThread(Arg.Any>>>())).Do(x => - { - var func = x.Arg>>>(); - func(); - }); - return mockedThreadHandling; - } - - private static ICancellableActionRunner CreateMockedActionRunner() - { - var mockedActionRunner = Substitute.For(); - mockedActionRunner.When(x => x.RunAsync(Arg.Any>())) - .Do(callInfo => - { - var func = callInfo.Arg>(); - func(new CancellationToken()); - }); - return mockedActionRunner; - } - - private void VerifySuppressedIssuesReloadedNotInvoked() => suppressedIssuesReloaded.DidNotReceiveWithAnyArgs().Invoke(testSubject, Arg.Any()); - - private void VerifySuppressedIssuesReloadedInvoked(IEnumerable expectedAllSuppressedIssues) => - suppressedIssuesReloaded.Received(1) - .Invoke(testSubject, Arg.Is(x => x.SuppressedIssues.SequenceEqual(expectedAllSuppressedIssues))); - - private void VerifySuppressionsRemovedNotInvoked() => suppressionsRemoved.DidNotReceiveWithAnyArgs().Invoke(testSubject, Arg.Any()); - - private void VerifySuppressionsRemovedInvoked(IEnumerable expectedSuppressedIssuesKeys) => - suppressionsRemoved.Received(1) - .Invoke(testSubject, Arg.Is(x => x.IssueServerKeys.SequenceEqual(expectedSuppressedIssuesKeys))); - - private void VerifyNewIssuesSuppressedInvoked(IEnumerable expectedAllSuppressedIssues) => - newIssuesSuppressed.Received(1) - .Invoke(testSubject, Arg.Is(x => x.SuppressedIssues.SequenceEqual(expectedAllSuppressedIssues))); - - private void VerifyNewIssuesSuppressedNotInvoked() => newIssuesSuppressed.DidNotReceiveWithAnyArgs().Invoke(testSubject, Arg.Any()); - - private void AssertMessageArgsDoesNotExist(params object[] args) => logger.DidNotReceive().WriteLine(Arg.Any(), Arg.Is(x => x.SequenceEqual(args))); - - private void AssertMessageArgsExists(params object[] args) => logger.Received(1).WriteLine(Arg.Any(), Arg.Is(x => x.SequenceEqual(args))); - - private void AssertMessageExists(string message) => logger.Received(1).WriteLine(Arg.Is(x => x == message), Arg.Any()); -} diff --git a/src/ConnectedMode.UnitTests/Suppressions/TimedUpdateHandlerTests.cs b/src/ConnectedMode.UnitTests/Suppressions/TimedUpdateHandlerTests.cs deleted file mode 100644 index ea9b2f4adb..0000000000 --- a/src/ConnectedMode.UnitTests/Suppressions/TimedUpdateHandlerTests.cs +++ /dev/null @@ -1,179 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.Core.SystemAbstractions; -using SonarLint.VisualStudio.TestInfrastructure; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Suppressions -{ - [TestClass] - public class TimedUpdateHandlerTests - { - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void MefCtor_CheckIsSingleton() - { - MefTestHelpers.CheckIsSingletonMefComponent(); - } - - [TestMethod] - public void Ctor_TimerIsSetupAsExpected() - { - var refreshTimer = new Mock(); - refreshTimer.SetupProperty(x => x.AutoReset); - refreshTimer.SetupProperty(x => x.Interval); - - var timerFactory = CreateTimerFactory(refreshTimer.Object); - var activeSolutionBoundTracker = CreateActiveSolutionBoundTrackerWihtBindingConfig(SonarLintMode.Connected); - - _ = CreateTestSubject(activeSolutionBoundTracker: activeSolutionBoundTracker.Object, timerFactory: timerFactory); - - refreshTimer.Object.AutoReset.Should().BeTrue(); - refreshTimer.Object.Interval.Should().Be(1000 * 60 * 10); - refreshTimer.VerifyAdd(x => x.Elapsed += It.IsAny>(), Times.Once); - refreshTimer.Verify(x => x.Start(), Times.Once); - } - - [TestMethod] - [DataRow(SonarLintMode.Standalone, false)] - [DataRow(SonarLintMode.Connected, true)] - [DataRow(SonarLintMode.LegacyConnected, true)] - public void Ctor_DependingOnBindingConfig_InitialTimeStateSetCorrectly(SonarLintMode mode, bool start) - { - var refreshTimer = new Mock(); - var timerFactory = CreateTimerFactory(refreshTimer.Object); - var activeSolutionBoundTracker = CreateActiveSolutionBoundTrackerWihtBindingConfig(mode); - - _ = CreateTestSubject(activeSolutionBoundTracker: activeSolutionBoundTracker.Object, timerFactory: timerFactory); - - refreshTimer.Verify(x => x.Start(), start ? Times.Once : Times.Never); - refreshTimer.Verify(x => x.Stop(), start ? Times.Never : Times.Once); - } - - [TestMethod] - public void InvokeEvent_TimerElapsed_StoreUpdatersAreCalled() - { - var refreshTimer = new Mock(); - var timerFactory = CreateTimerFactory(refreshTimer.Object); - var activeSolutionBoundTracker = CreateActiveSolutionBoundTrackerWihtBindingConfig(SonarLintMode.Connected); - var suppressionUpdater = new Mock(); - var qualityProfileUpdater = new Mock(); - - _ = CreateTestSubject(activeSolutionBoundTracker.Object, suppressionUpdater.Object, qualityProfileUpdater.Object, timerFactory); - - refreshTimer.Raise(x => x.Elapsed += null, new TimerEventArgs(DateTime.UtcNow)); - - suppressionUpdater.Verify(x => x.UpdateAllServerSuppressionsAsync(), Times.Once); - qualityProfileUpdater.Verify(x => x.UpdateAsync(), Times.Once); - } - - [TestMethod] - public void Dispose_RefreshTimerDisposed_RaisingEventDoesNothing() - { - var refreshTimer = new Mock(); - var timerFactory = CreateTimerFactory(refreshTimer.Object); - var activeSolutionBoundTracker = CreateActiveSolutionBoundTrackerWihtBindingConfig(SonarLintMode.Connected); - var suppressionUpdater = new Mock(); - var qualityProfileUpdater = new Mock(); - - var testSubject = CreateTestSubject(activeSolutionBoundTracker.Object, suppressionUpdater.Object, qualityProfileUpdater.Object, timerFactory); - - testSubject.Dispose(); - - refreshTimer.Verify(x => x.Dispose(), Times.Once); - refreshTimer.Raise(x => x.Elapsed += null, new TimerEventArgs(DateTime.UtcNow)); - - suppressionUpdater.Verify(x => x.UpdateAllServerSuppressionsAsync(), Times.Never); - qualityProfileUpdater.Verify(x => x.UpdateAsync(), Times.Never); - } - - [TestMethod] - public void InvokeBindingChanged_TimerStartsStopsOnActiveSolutionBoundChange() - { - var refreshTimer = new Mock(); - var timerFactory = CreateTimerFactory(refreshTimer.Object); - var activeSolutionBoundTracker = CreateActiveSolutionBoundTrackerWihtBindingConfig(SonarLintMode.Standalone); - - _ = CreateTestSubject(activeSolutionBoundTracker: activeSolutionBoundTracker.Object, timerFactory: timerFactory); - - refreshTimer.Reset(); - - activeSolutionBoundTracker.Raise(x => x.SolutionBindingChanged += null, new ActiveSolutionBindingEventArgs(CreateBindingConfiguration(SonarLintMode.Connected))); - refreshTimer.Verify(x => x.Start(), Times.Once); - refreshTimer.Verify(x => x.Stop(), Times.Never); - - activeSolutionBoundTracker.Raise(x => x.SolutionBindingChanged += null, new ActiveSolutionBindingEventArgs(CreateBindingConfiguration(SonarLintMode.Standalone))); - refreshTimer.Verify(x => x.Start(), Times.Once); - refreshTimer.Verify(x => x.Stop(), Times.Once); - } - - private static ITimerFactory CreateTimerFactory(ITimer timer) - { - var timerFactory = new Mock(); - - timerFactory.Setup(x => x.Create()).Returns(timer); - - return timerFactory.Object; - } - - private static TimedUpdateHandler CreateTestSubject( - IActiveSolutionBoundTracker activeSolutionBoundTracker, - IRoslynSuppressionUpdater roslynSuppressionUpdater = null, - IQualityProfileUpdater qualityProfileUpdater = null, - ITimerFactory timerFactory = null) - { - return new TimedUpdateHandler( - roslynSuppressionUpdater ?? Mock.Of(), - qualityProfileUpdater ?? Mock.Of(), - activeSolutionBoundTracker, - new TestLogger(logToConsole: true), - timerFactory ?? Mock.Of()); - } - - private BindingConfiguration CreateBindingConfiguration(SonarLintMode mode) - { - return new BindingConfiguration(new BoundServerProject("solution", "projectKey", new ServerConnection.SonarQube(new Uri("http://localhost"))), mode, ""); - } - - private Mock CreateActiveSolutionBoundTrackerWihtBindingConfig(SonarLintMode mode) - { - var activeSolutionTracker = new Mock(); - - var bindingConfig = CreateBindingConfiguration(mode); - - activeSolutionTracker.Setup(x => x.CurrentConfiguration).Returns(bindingConfig); - - return activeSolutionTracker; - } - } -} diff --git a/src/ConnectedMode.UnitTests/Transition/MuteIssuesServiceTests.cs b/src/ConnectedMode.UnitTests/Transition/MuteIssuesServiceTests.cs index 92e18ebbdc..8589e148f5 100644 --- a/src/ConnectedMode.UnitTests/Transition/MuteIssuesServiceTests.cs +++ b/src/ConnectedMode.UnitTests/Transition/MuteIssuesServiceTests.cs @@ -20,12 +20,10 @@ using NSubstitute.ExceptionExtensions; using NSubstitute.ReturnsExtensions; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; using SonarLint.VisualStudio.ConnectedMode.Transition; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Analysis; using SonarLint.VisualStudio.Core.ConfigurationScope; -using SonarLint.VisualStudio.Core.Suppressions; using SonarLint.VisualStudio.IssueVisualization.Models; using SonarLint.VisualStudio.SLCore; using SonarLint.VisualStudio.SLCore.Core; @@ -40,9 +38,7 @@ namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Transition; public class MuteIssuesServiceTests { private const string AnIssueServerKey = "issueServerKey"; - private const string RoslynIssueServerKey = "roslynKey"; private readonly IAnalysisIssueVisualization nonRoslynIssue = Substitute.For(); - private readonly IFilterableRoslynIssue roslynIssue = Substitute.For(); private MuteIssuesService testSubject; private IMuteIssuesWindowService muteIssuesWindowService; @@ -51,8 +47,6 @@ public class MuteIssuesServiceTests private TestLogger logger; private IThreadHandling threadHandling; private IIssueSLCoreService issueSlCoreService; - private IServerIssueFinder serverIssueFinder; - private IRoslynSuppressionUpdater roslynSuppressionUpdater; [TestInitialize] public void TestInitialize() @@ -60,12 +54,10 @@ public void TestInitialize() muteIssuesWindowService = Substitute.For(); activeConfigScopeTracker = Substitute.For(); slCoreServiceProvider = Substitute.For(); - serverIssueFinder = Substitute.For(); - roslynSuppressionUpdater = Substitute.For(); logger = new TestLogger(); threadHandling = new NoOpThreadHandler(); issueSlCoreService = Substitute.For(); - testSubject = new MuteIssuesService(muteIssuesWindowService, activeConfigScopeTracker, slCoreServiceProvider, serverIssueFinder, roslynSuppressionUpdater, logger, + testSubject = new MuteIssuesService(muteIssuesWindowService, activeConfigScopeTracker, slCoreServiceProvider, logger, threadHandling); MockNonRoslynIssue(); @@ -83,8 +75,6 @@ public void MefCtor_CheckIsExported() => MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); @@ -96,7 +86,7 @@ public void Logger_HasCorrectContext() { var substituteLogger = Substitute.For(); - _ = new MuteIssuesService(muteIssuesWindowService, activeConfigScopeTracker, slCoreServiceProvider, serverIssueFinder, roslynSuppressionUpdater, substituteLogger, + _ = new MuteIssuesService(muteIssuesWindowService, activeConfigScopeTracker, slCoreServiceProvider, substituteLogger, threadHandling); substituteLogger.Received(1).ForContext("MuteIssuesService"); @@ -115,31 +105,6 @@ public void ResolveIssueWithDialogAsync_WhenIssueServerKeyIsNull_LogsAndThrows(s logger.AssertPartialOutputStrings(Resources.MuteIssue_IssueNotFound); } - [TestMethod] - [DataRow(null)] - [DataRow("")] - public void ResolveIssueWithDialogAsync_WhenRoslynIssueServerKeyIsNull_LogsAndThrows(string issueServerKey) - { - MockRoslynIssueOnServer(issueServerKey); - - var act = () => testSubject.ResolveIssueWithDialogAsync(roslynIssue); - - act.Should().Throw().WithMessage(Resources.MuteIssue_IssueNotFound); - logger.AssertPartialOutputStrings(Resources.MuteIssue_IssueNotFound); - } - - [TestMethod] - public void ResolveIssueWithDialogAsync_WhenRoslynIssueServerIsAlreadyResolvedOnServer_LogsAndThrows() - { - var serverIssue = MockRoslynIssueOnServer(RoslynIssueServerKey); - serverIssue.IsResolved = true; - - var act = () => testSubject.ResolveIssueWithDialogAsync(roslynIssue); - - act.Should().Throw().WithMessage(Resources.MuteIssue_ErrorIssueAlreadyResolved); - logger.AssertPartialOutputStrings(Resources.MuteIssue_ErrorIssueAlreadyResolved); - } - [TestMethod] public void ResolveIssueWithDialogAsync_WhenConfigScopeIsNotSet_LogsAndThrows() { @@ -237,25 +202,6 @@ public void ResolveIssueWithDialogAsync_WhenWindowResponseResultIsTrue_ShouldMut && !x.isTaintIssue)); } - [TestMethod] - [DataRow(ResolutionStatus.ACCEPT, SonarQubeIssueTransition.Accept)] - [DataRow(ResolutionStatus.WONT_FIX, SonarQubeIssueTransition.WontFix)] - [DataRow(ResolutionStatus.FALSE_POSITIVE, SonarQubeIssueTransition.FalsePositive)] - public void ResolveIssueWithDialogAsync_WhenWindowResponseResultIsTrue_AndRoslynIssue_ShouldMuteIssue(ResolutionStatus resolutionStatus, SonarQubeIssueTransition transition) - { - MuteIssuePermitted(); - MockRoslynIssueOnServer(RoslynIssueServerKey); - muteIssuesWindowService.Show(Arg.Any>()).Returns(new MuteIssuesWindowResponse { Result = true, IssueTransition = transition }); - - _ = testSubject.ResolveIssueWithDialogAsync(roslynIssue); - - issueSlCoreService.Received().ChangeStatusAsync(Arg.Is(x => - x.issueKey == RoslynIssueServerKey - && x.newStatus == resolutionStatus - && x.configurationScopeId == "CONFIG_SCOPE_ID" - && !x.isTaintIssue)); - } - [TestMethod] public void ResolveIssueWithDialogAsync_WhenWindowResponseHasComment_ShouldAddComment() { @@ -322,56 +268,6 @@ public void ResolveIssueWithDialogAsync_WhenMuteIssueFails_LogsAndThrows() logger.AssertPartialOutputStrings("Some error"); } - [TestMethod] - public void ResolveIssueWithDialogAsync_RoslynIssueMutedSuccessfully_CallsSuppressionsUpdater() - { - MuteIssuePermitted(); - MockRoslynIssueOnServer(RoslynIssueServerKey); - MockIssueAcceptedInWindow(); - - _ = testSubject.ResolveIssueWithDialogAsync(roslynIssue); - - roslynSuppressionUpdater.Received(1).UpdateSuppressedIssuesAsync(isResolved: true, Arg.Is(x => x.SequenceEqual(new[] { RoslynIssueServerKey })), Arg.Any()); - } - - [TestMethod] - public void ResolveIssueWithDialogAsync_NonRoslynIssueMutedSuccessfully_DoesNotCallSuppressionsUpdater() - { - MuteIssuePermitted(); - MockIssueAcceptedInWindow(); - - _ = testSubject.ResolveIssueWithDialogAsync(nonRoslynIssue); - - roslynSuppressionUpdater.DidNotReceiveWithAnyArgs().UpdateSuppressedIssuesAsync(default, default, default); - } - - [TestMethod] - public void ResolveIssueWithDialogAsync_RoslynIssueMutedSuccessfullyButCommentFails_CallsSuppressionsUpdater() - { - MuteIssuePermitted(); - MockRoslynIssueOnServer(RoslynIssueServerKey); - const string comment = "No you are not an issue, you are a feature"; - MockIssueAcceptedInWindow(comment); - issueSlCoreService.AddCommentAsync(Arg.Any()).ThrowsAsync(new Exception("Some error")); - - _ = testSubject.ResolveIssueWithDialogAsync(roslynIssue); - - roslynSuppressionUpdater.Received(1).UpdateSuppressedIssuesAsync(isResolved: true, Arg.Is(x => x.SequenceEqual(new[] { RoslynIssueServerKey })), Arg.Any()); - } - - [TestMethod] - public void ResolveIssueWithDialogAsync_NonRoslynIssueMutedSuccessfullyButCommentFails_DoesCallSuppressionsUpdater() - { - MuteIssuePermitted(); - const string comment = "No you are not an issue, you are a feature"; - MockIssueAcceptedInWindow(comment); - issueSlCoreService.AddCommentAsync(Arg.Any()).ThrowsAsync(new Exception("Some error")); - - _ = testSubject.ResolveIssueWithDialogAsync(nonRoslynIssue); - - roslynSuppressionUpdater.DidNotReceive().UpdateSuppressedIssuesAsync(Arg.Any(), Arg.Any(), Arg.Any()); - } - private void NotInConnectedMode() => activeConfigScopeTracker.Current.Returns(new Core.ConfigurationScope.ConfigurationScope("CONFIG_SCOPE_ID")); private void ServiceProviderNotInitialized() => slCoreServiceProvider.TryGetTransientService(out Arg.Any()).ReturnsForAnyArgs(false); @@ -390,6 +286,10 @@ private void AssertMuteIssueWithoutComment() issueSlCoreService.DidNotReceiveWithAnyArgs().AddCommentAsync(Arg.Any()); } + private void MockIssueAcceptedInWindow(string comment = null) => + muteIssuesWindowService.Show(Arg.Any>()) + .Returns(new MuteIssuesWindowResponse { Result = true, IssueTransition = SonarQubeIssueTransition.Accept, Comment = comment }); + private void MockNonRoslynIssue() { var analysisBase = Substitute.For(); @@ -397,19 +297,4 @@ private void MockNonRoslynIssue() nonRoslynIssue.Issue.Returns(analysisBase); nonRoslynIssue.FilePath.Returns("C:\\somePath.cs"); } - - private SonarQubeIssue MockRoslynIssueOnServer(string issueServerKey) - { - roslynIssue.FilePath.Returns("C:\\someOtherPath.cs"); - var serverIssue = CreateServerIssue(issueServerKey); - serverIssueFinder.FindServerIssueAsync(roslynIssue, Arg.Any()).Returns(serverIssue); - return serverIssue; - } - - public static SonarQubeIssue CreateServerIssue(string issueKey, bool isResolved = false) => - new(issueKey, default, default, default, default, default, isResolved, default, default, default, default, default); - - private void MockIssueAcceptedInWindow(string comment = null) => - muteIssuesWindowService.Show(Arg.Any>()) - .Returns(new MuteIssuesWindowResponse { Result = true, IssueTransition = SonarQubeIssueTransition.Accept, Comment = comment }); } diff --git a/src/ConnectedMode.UnitTests/packages.lock.json b/src/ConnectedMode.UnitTests/packages.lock.json index eb0a0b7c95..d252cde329 100644 --- a/src/ConnectedMode.UnitTests/packages.lock.json +++ b/src/ConnectedMode.UnitTests/packages.lock.json @@ -162,16 +162,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "Humanizer.Core": { "type": "Transitive", "resolved": "2.2.0", @@ -1562,8 +1552,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/ConnectedMode/Binding/BindingProcessImpl.cs b/src/ConnectedMode/Binding/BindingProcessImpl.cs deleted file mode 100644 index 4386966c7b..0000000000 --- a/src/ConnectedMode/Binding/BindingProcessImpl.cs +++ /dev/null @@ -1,62 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.Core; - -namespace SonarLint.VisualStudio.ConnectedMode.Binding -{ - internal class BindingProcessImpl : IBindingProcess - { - private readonly BindCommandArgs bindingArgs; - private readonly IQualityProfileDownloader qualityProfileDownloader; - private readonly ILogger logger; - - public BindingProcessImpl( - BindCommandArgs bindingArgs, - IQualityProfileDownloader qualityProfileDownloader, - ILogger logger) - { - this.bindingArgs = bindingArgs ?? throw new ArgumentNullException(nameof(bindingArgs)); - this.qualityProfileDownloader = qualityProfileDownloader ?? throw new ArgumentNullException(nameof(qualityProfileDownloader)); - this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - #region IBindingTemplate methods - - public async Task DownloadQualityProfileAsync(IProgress progress, CancellationToken cancellationToken) - { - try - { - await qualityProfileDownloader.UpdateAsync(bindingArgs.ProjectToBind, progress, cancellationToken); - // ignore the UpdateAsync result, as the return value of false indicates error, rather than lack of changes - return true; - } - catch (InvalidOperationException e) - { - logger.LogVerbose($"[{nameof(BindingProcessImpl)}] {e}"); - } - - return false; - } - - #endregion - } -} diff --git a/src/ConnectedMode/Binding/BoundSonarQubeProject.cs b/src/ConnectedMode/Binding/BoundSonarQubeProject.cs index abcc8c4b26..73f3f103ee 100644 --- a/src/ConnectedMode/Binding/BoundSonarQubeProject.cs +++ b/src/ConnectedMode/Binding/BoundSonarQubeProject.cs @@ -59,7 +59,6 @@ public BoundSonarQubeProject( public string ProjectKey { get; set; } public string ProjectName { get; set; } - public Dictionary Profiles { get; set; } [JsonIgnore] public IConnectionCredentials Credentials { get; set; } diff --git a/src/ConnectedMode/Binding/BoundSonarQubeProjectExtensions.cs b/src/ConnectedMode/Binding/BoundSonarQubeProjectExtensions.cs index c9acb15b7e..2c00cafcb0 100644 --- a/src/ConnectedMode/Binding/BoundSonarQubeProjectExtensions.cs +++ b/src/ConnectedMode/Binding/BoundSonarQubeProjectExtensions.cs @@ -36,7 +36,7 @@ public static ServerConnection FromBoundSonarQubeProject(this BoundSonarQubeProj public static BoundServerProject FromBoundSonarQubeProject(this BoundSonarQubeProject boundProject, string localBindingKey, ServerConnection connection) => new(localBindingKey ?? throw new ArgumentNullException(nameof(localBindingKey)), boundProject?.ProjectKey ?? throw new ArgumentNullException(nameof(boundProject)), - connection ?? throw new ArgumentNullException(nameof(connection))) { Profiles = boundProject.Profiles }; + connection ?? throw new ArgumentNullException(nameof(connection))); public static ConnectionInformation CreateConnectionInformation(this BoundSonarQubeProject binding) { diff --git a/src/ConnectedMode/Binding/IBindingProcess.cs b/src/ConnectedMode/Binding/IBindingProcess.cs deleted file mode 100644 index c94b5b50bb..0000000000 --- a/src/ConnectedMode/Binding/IBindingProcess.cs +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace SonarLint.VisualStudio.ConnectedMode.Binding -{ - /// - /// Encapsulates the set of operations carried out in sequence by the binding workflow. - /// The operations are expected to be called in the order in which they are defined in this file. - /// - /// This interface as extracted from the class to reduce - /// the complexity of that class and simplify testing. The class is responsible - /// for handling threading and progress reporting in the UI. This class is responsible for the - /// making the changes to projects and files. - internal interface IBindingProcess - { - Task DownloadQualityProfileAsync(IProgress progress, CancellationToken cancellationToken); - } -} diff --git a/src/ConnectedMode/Binding/IBindingProcessFactory.cs b/src/ConnectedMode/Binding/IBindingProcessFactory.cs deleted file mode 100644 index dff4b88da2..0000000000 --- a/src/ConnectedMode/Binding/IBindingProcessFactory.cs +++ /dev/null @@ -1,58 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.Core; - -namespace SonarLint.VisualStudio.ConnectedMode.Binding -{ - /// - /// Factory to create a new binding process - /// - internal interface IBindingProcessFactory - { - IBindingProcess Create(BindCommandArgs bindingArgs); - } - - [Export(typeof(IBindingProcessFactory))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal class BindingProcessFactory : IBindingProcessFactory - { - private readonly IQualityProfileDownloader qualityProfileDownloader; - private readonly ILogger logger; - - [ImportingConstructor] - public BindingProcessFactory( - IQualityProfileDownloader qualityProfileDownloader, - ILogger logger) - { - this.qualityProfileDownloader = qualityProfileDownloader; - this.logger = logger; - } - - public IBindingProcess Create(BindCommandArgs bindingArgs) - { - return new BindingProcessImpl(bindingArgs, - qualityProfileDownloader, - logger); - } - } -} diff --git a/src/ConnectedMode/Binding/IUnintrusiveBindingController.cs b/src/ConnectedMode/Binding/IUnintrusiveBindingController.cs index 84ae87eab9..3095191ca4 100644 --- a/src/ConnectedMode/Binding/IUnintrusiveBindingController.cs +++ b/src/ConnectedMode/Binding/IUnintrusiveBindingController.cs @@ -34,31 +34,25 @@ public interface IBindingController bool Unbind(string localBindingKey); } - internal interface IUnintrusiveBindingController - { - Task BindAsync(BoundServerProject project, IProgress progress, CancellationToken token); - } - [Export(typeof(IBindingController))] - [Export(typeof(IUnintrusiveBindingController))] [PartCreationPolicy(CreationPolicy.NonShared)] - internal class UnintrusiveBindingController : IUnintrusiveBindingController, IBindingController + internal class UnintrusiveBindingController : IBindingController { - private readonly IBindingProcessFactory bindingProcessFactory; private readonly ISonarQubeService sonarQubeService; private readonly IActiveSolutionChangedHandler activeSolutionChangedHandler; + private readonly IConfigurationPersister configurationPersister; private readonly ISolutionBindingRepository solutionBindingRepository; [ImportingConstructor] public UnintrusiveBindingController( - IBindingProcessFactory bindingProcessFactory, ISonarQubeService sonarQubeService, IActiveSolutionChangedHandler activeSolutionChangedHandler, - ISolutionBindingRepository solutionBindingRepository) + ISolutionBindingRepository solutionBindingRepository, + IConfigurationPersister configurationPersister) { - this.bindingProcessFactory = bindingProcessFactory; this.sonarQubeService = sonarQubeService; this.activeSolutionChangedHandler = activeSolutionChangedHandler; + this.configurationPersister = configurationPersister; this.solutionBindingRepository = solutionBindingRepository; } @@ -66,16 +60,10 @@ public async Task BindAsync(BoundServerProject project, CancellationToken cancel { var connectionInformation = new ConnectionInformation(project.ServerConnection.ServerUri, project.ServerConnection.Credentials); await sonarQubeService.ConnectAsync(connectionInformation, cancellationToken); - await BindAsync(project, null, cancellationToken); + configurationPersister.Persist(project); activeSolutionChangedHandler.HandleBindingChange(); } - public async Task BindAsync(BoundServerProject project, IProgress progress, CancellationToken token) - { - var bindingProcess = CreateBindingProcess(project); - await bindingProcess.DownloadQualityProfileAsync(progress, token); - } - public bool Unbind(string localBindingKey) { var bindingDeleted = solutionBindingRepository.DeleteBinding(localBindingKey); @@ -86,12 +74,5 @@ public bool Unbind(string localBindingKey) } return bindingDeleted; } - - private IBindingProcess CreateBindingProcess(BoundServerProject project) - { - var bindingProcess = bindingProcessFactory.Create(new BindCommandArgs(project)); - - return bindingProcess; - } } } diff --git a/src/ConnectedMode/Binding/RoslynBindingConfigProvider.cs b/src/ConnectedMode/Binding/RoslynBindingConfigProvider.cs deleted file mode 100644 index 7a809b8058..0000000000 --- a/src/ConnectedMode/Binding/RoslynBindingConfigProvider.cs +++ /dev/null @@ -1,143 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using SonarLint.VisualStudio.ConnectedMode.Helpers; -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.Core.CSharpVB; -using SonarQube.Client; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode.Binding; - -/// -/// Turn server Quality Profile information into a set of config files for a particular language -/// -[Export(typeof(IBindingConfigProvider))] -[PartCreationPolicy(CreationPolicy.Shared)] -[method: ImportingConstructor] -internal class RoslynBindingConfigProvider( - ISonarQubeService sonarQubeService, - ILogger logger, - IRoslynConfigGenerator roslynConfigGenerator, - ILanguageProvider languageProvider) - : IBindingConfigProvider -{ - private const string TaintAnalysisRepoPrefix = "roslyn.sonaranalyzer.security."; - private readonly IEnvironmentSettings environmentSettings = new EnvironmentSettings(); - - public bool IsLanguageSupported(Language language) => languageProvider.RoslynLanguages.Contains(language); - - public Task SaveConfigurationAsync( - SonarQubeQualityProfile qualityProfile, - Language language, - BindingConfiguration bindingConfiguration, - CancellationToken cancellationToken) - { - if (!IsLanguageSupported(language)) - { - throw new ArgumentOutOfRangeException(nameof(language)); - } - - return SaveConfigurationInternalAsync(qualityProfile, language, bindingConfiguration, cancellationToken); - } - - private async Task SaveConfigurationInternalAsync( - SonarQubeQualityProfile qualityProfile, - Language language, - BindingConfiguration bindingConfiguration, - CancellationToken cancellationToken) - { - var serverLanguageKey = language.ServerLanguageKey; - Debug.Assert(serverLanguageKey != null, - $"Server language should not be null for supported language: {language.Id}"); - - // First, fetch the active rules - var activeRules = (await FetchSupportedRulesAsync(true, qualityProfile.Key, cancellationToken)).ToList(); - - // Give up if the quality profile is empty - no point in fetching anything else - if (!activeRules.Any()) - { - logger.WriteLine(string.Format(BindingStrings.SubTextPaddingFormat, - string.Format(BindingStrings.NoSonarAnalyzerActiveRulesForQualityProfile, qualityProfile.Name, language.Name))); - // NOTE: this should never happen, binding config should be present for every supported language - throw new InvalidOperationException( - string.Format(QualityProfilesStrings.FailedToCreateBindingConfigForLanguage, language.Name)); - } - - // Now fetch the data required for the NuGet configuration - var sonarProperties = await FetchPropertiesAsync(bindingConfiguration.Project.ServerProjectKey, cancellationToken); - - // Finally, fetch the remaining data needed to build the globalconfig - var inactiveRules = await FetchSupportedRulesAsync(false, qualityProfile.Key, cancellationToken); - var exclusions = await FetchInclusionsExclusionsAsync(bindingConfiguration.Project.ServerProjectKey, cancellationToken); - - roslynConfigGenerator.GenerateAndSaveConfiguration( - language, - bindingConfiguration.BindingConfigDirectory, - sonarProperties, - exclusions, - activeRules.Union(inactiveRules).Select(x => new SonarQubeRoslynRuleStatus(x, environmentSettings)).ToList(), - activeRules); - } - - private async Task FetchInclusionsExclusionsAsync( - string projectKey, - CancellationToken cancellationToken) - { - var exclusions = await WebServiceHelper.SafeServiceCallAsync( - () => sonarQubeService.GetServerExclusions(projectKey, cancellationToken), logger); - - return exclusions; - } - - private async Task> FetchSupportedRulesAsync(bool active, string qpKey, CancellationToken cancellationToken) - { - var rules = await WebServiceHelper.SafeServiceCallAsync( - () => sonarQubeService.GetRulesAsync(active, qpKey, cancellationToken), logger); - return rules.Where(IsSupportedRule).ToArray(); - } - - private async Task> FetchPropertiesAsync(string projectKey, CancellationToken cancellationToken) - { - var serverProperties = await WebServiceHelper.SafeServiceCallAsync( - () => sonarQubeService.GetAllPropertiesAsync(projectKey, cancellationToken), logger); - - return serverProperties.ToDictionary(x => x.Key, x => x.Value); - } - - internal static /* for testing */ bool IsSupportedRule(SonarQubeRule rule) - { - // We don't want to generate configuration for taint-analysis rules or hotspots. - // * taint-analysis rules: these are in a separate analyzer that doesn't ship in SLVS so there is no point in generating config - // * hotspots: these are noisy so we don't want to run them in the IDE. There is special code in the Sonar hotspot analyzers to - // control when they run; we are responsible for not generating configuration for them. - return IsSupportedIssueType(rule.IssueType) && !IsTaintAnalysisRule(rule); - } - - private static bool IsTaintAnalysisRule(SonarQubeRule rule) => rule.RepositoryKey.StartsWith(TaintAnalysisRepoPrefix, StringComparison.OrdinalIgnoreCase); - - private static bool IsSupportedIssueType(SonarQubeIssueType issueType) => - issueType == SonarQubeIssueType.CodeSmell || - issueType == SonarQubeIssueType.Bug || - issueType == SonarQubeIssueType.Vulnerability; -} diff --git a/src/ConnectedMode/Binding/SonarQubeRuleStatus.cs b/src/ConnectedMode/Binding/SonarQubeRuleStatus.cs deleted file mode 100644 index 28fbd16b4d..0000000000 --- a/src/ConnectedMode/Binding/SonarQubeRuleStatus.cs +++ /dev/null @@ -1,62 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.CSharpVB; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode.Binding; - -public class SonarQubeRoslynRuleStatus(SonarQubeRule sonarQubeRule, IEnvironmentSettings environmentSettings) : IRoslynRuleStatus -{ - public string Key => sonarQubeRule.Key; - - public RuleAction GetSeverity() - { - if (!sonarQubeRule.IsActive) - { - return RuleAction.None; - } - return GetVsSeverity(sonarQubeRule.SoftwareQualitySeverities?.Values) ?? GetVsSeverity(sonarQubeRule.Severity); - } - - private RuleAction? GetVsSeverity(ICollection severities) => - severities is not { Count: > 0 } - ? null - : GetVsSeverity(severities.Max()); - - private RuleAction GetVsSeverity(SonarQubeSoftwareQualitySeverity severity) => - severity switch - { - SonarQubeSoftwareQualitySeverity.Blocker or SonarQubeSoftwareQualitySeverity.High => environmentSettings.TreatBlockerSeverityAsError() ? RuleAction.Error : RuleAction.Warning, - SonarQubeSoftwareQualitySeverity.Medium => RuleAction.Warning, - SonarQubeSoftwareQualitySeverity.Low or SonarQubeSoftwareQualitySeverity.Info => RuleAction.Info, - _ => throw new ArgumentOutOfRangeException($"Unsupported SonarQube issue severity: {severity}") - }; - - private RuleAction GetVsSeverity(SonarQubeIssueSeverity sqSeverity) => - sqSeverity switch - { - SonarQubeIssueSeverity.Info or SonarQubeIssueSeverity.Minor => RuleAction.Info, - SonarQubeIssueSeverity.Major or SonarQubeIssueSeverity.Critical => RuleAction.Warning, - SonarQubeIssueSeverity.Blocker => environmentSettings.TreatBlockerSeverityAsError() ? RuleAction.Error : RuleAction.Warning, - _ => throw new ArgumentOutOfRangeException($"Unsupported SonarQube issue severity: {sqSeverity}") - }; -} diff --git a/src/ConnectedMode/BoundSolutionUpdateHandler.cs b/src/ConnectedMode/BoundSolutionUpdateHandler.cs deleted file mode 100644 index bd643efe18..0000000000 --- a/src/ConnectedMode/BoundSolutionUpdateHandler.cs +++ /dev/null @@ -1,73 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using Microsoft.VisualStudio.Threading; -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; -using SonarLint.VisualStudio.Core.Binding; - -namespace SonarLint.VisualStudio.ConnectedMode -{ - [Export(typeof(BoundSolutionUpdateHandler))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal sealed class BoundSolutionUpdateHandler : IDisposable - { - private readonly IActiveSolutionBoundTracker activeSolutionBoundTracker; - private readonly IRoslynSuppressionUpdater roslynSuppressionUpdater; - private readonly IQualityProfileUpdater qualityProfileUpdater; - - private bool disposed; - - [ImportingConstructor] - public BoundSolutionUpdateHandler( - IActiveSolutionBoundTracker activeSolutionBoundTracker, - IRoslynSuppressionUpdater roslynSuppressionUpdater, - IQualityProfileUpdater qualityProfileUpdater) - { - this.activeSolutionBoundTracker = activeSolutionBoundTracker; - this.roslynSuppressionUpdater = roslynSuppressionUpdater; - this.qualityProfileUpdater = qualityProfileUpdater; - - this.activeSolutionBoundTracker.SolutionBindingChanged += OnSolutionBindingChanged; - this.activeSolutionBoundTracker.SolutionBindingUpdated += OnSolutionBindingUpdated; - } - - private void OnSolutionBindingUpdated(object sender, EventArgs e) => TriggerUpdate(); - - private void OnSolutionBindingChanged(object sender, ActiveSolutionBindingEventArgs e) => TriggerUpdate(); - - private void TriggerUpdate() - { - roslynSuppressionUpdater.UpdateAllServerSuppressionsAsync().Forget(); - qualityProfileUpdater.UpdateAsync().Forget(); - } - - public void Dispose() - { - if (!disposed) - { - activeSolutionBoundTracker.SolutionBindingChanged -= OnSolutionBindingChanged; - activeSolutionBoundTracker.SolutionBindingUpdated -= OnSolutionBindingUpdated; - disposed = true; - } - } - } -} diff --git a/src/ConnectedMode/BranchMatcher.cs b/src/ConnectedMode/BranchMatcher.cs index 2684f2963e..dadb3cf145 100644 --- a/src/ConnectedMode/BranchMatcher.cs +++ b/src/ConnectedMode/BranchMatcher.cs @@ -18,20 +18,15 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using System.ComponentModel.Composition; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using LibGit2Sharp; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.ETW; -using SonarQube.Client; +using SonarLint.VisualStudio.SLCore.Listener.Branch; namespace SonarLint.VisualStudio.ConnectedMode { - public interface IBranchMatcher + internal interface IBranchMatcher { /// /// Calculates the Sonar server branch that is the closest match to the head branch. @@ -43,7 +38,7 @@ public interface IBranchMatcher /// This is the same algorithm as the other SonarLint flavours (?except in how we treated branch histories. /// We treat branch histories as series of linear commits ordered by time, even if there are merges). /// - Task GetMatchingBranch(string projectKey, IRepository gitRepo, CancellationToken token); + string GetMatchingBranch(string projectKey, IRepository gitRepo, List branches); } [Export(typeof(IBranchMatcher))] @@ -51,30 +46,23 @@ public interface IBranchMatcher [PartCreationPolicy(CreationPolicy.Shared)] internal class BranchMatcher : IBranchMatcher { - private const string shortBranchType = "SHORT"; - - private readonly ISonarQubeService sonarQubeService; private readonly ILogger logger; [ImportingConstructor] - public BranchMatcher(ISonarQubeService sonarQubeService, ILogger logger) + public BranchMatcher(ILogger logger) { - this.sonarQubeService = sonarQubeService; this.logger = logger; } - public async Task GetMatchingBranch(string projectKey, IRepository gitRepo, CancellationToken token) + public string GetMatchingBranch(string projectKey, IRepository gitRepo, List branches) { - Debug.Assert(sonarQubeService.IsConnected, - "Not expecting GetMatchingBranch to be called unless we are in Connected Mode"); - logger.LogVerbose(Resources.BranchMapper_CalculatingServerBranch_Started); string closestBranch; try { CodeMarkers.Instance.GetMatchingBranchStart(projectKey); - closestBranch = await DoGetMatchingBranch(projectKey, gitRepo, token); + closestBranch = DoGetMatchingBranch(gitRepo, branches); } finally { @@ -85,7 +73,9 @@ public async Task GetMatchingBranch(string projectKey, IRepository gitRe return closestBranch; } - private async Task DoGetMatchingBranch(string projectKey, IRepository gitRepo, CancellationToken token) + private string DoGetMatchingBranch( + IRepository gitRepo, + List branches) { var head = gitRepo.Head; @@ -95,9 +85,8 @@ private async Task DoGetMatchingBranch(string projectKey, IRepository gi return null; } - var remoteBranches = (await sonarQubeService.GetProjectBranchesAsync(projectKey, token)).Where(b => b.Type != shortBranchType); - if (remoteBranches.Any(rb => string.Equals(rb.Name, head.FriendlyName, StringComparison.InvariantCultureIgnoreCase))) + if (branches.Any(rb => string.Equals(rb.Name, head.FriendlyName, StringComparison.InvariantCultureIgnoreCase))) { logger.LogVerbose(Resources.BranchMapper_Match_SameSonarBranchName, head.FriendlyName); return head.FriendlyName; @@ -109,7 +98,7 @@ private async Task DoGetMatchingBranch(string projectKey, IRepository gi Lazy headCommits = new Lazy(() => head.Commits.ToArray()); logger.LogVerbose(Resources.BranchMapper_CheckingSonarBranches); - foreach (var remoteBranch in remoteBranches) + foreach (var remoteBranch in branches) { var localBranch = gitRepo.Branches.FirstOrDefault(r => string.Equals(r.FriendlyName, remoteBranch.Name, StringComparison.InvariantCultureIgnoreCase)); @@ -128,7 +117,7 @@ private async Task DoGetMatchingBranch(string projectKey, IRepository gi if (closestBranch is null) { logger.LogVerbose(Resources.BranchMapper_NoMatchingBranchFound); - closestBranch = remoteBranches.First(rb => rb.IsMain).Name; + closestBranch = branches.First(rb => rb.IsMain).Name; } return closestBranch; diff --git a/src/ConnectedMode/ConnectedMode.csproj b/src/ConnectedMode/ConnectedMode.csproj index e0affdf10b..6e4c445304 100644 --- a/src/ConnectedMode/ConnectedMode.csproj +++ b/src/ConnectedMode/ConnectedMode.csproj @@ -129,11 +129,6 @@ True PersistenceStrings.resx - - QualityProfilesStrings.resx - True - True - True True @@ -159,11 +154,6 @@ ResXFileCodeGenerator PersistenceStrings.Designer.cs - - Designer - QualityProfilesStrings.Designer.cs - ResXFileCodeGenerator - ResXFileCodeGenerator Resources.Designer.cs diff --git a/src/ConnectedMode/ConnectedModePackage.cs b/src/ConnectedMode/ConnectedModePackage.cs index ad9970ba2b..139c4541d9 100644 --- a/src/ConnectedMode/ConnectedModePackage.cs +++ b/src/ConnectedMode/ConnectedModePackage.cs @@ -22,14 +22,10 @@ using System.Runtime.InteropServices; using Microsoft.VisualStudio.ComponentModelHost; using Microsoft.VisualStudio.Shell; -using Microsoft.VisualStudio.Threading; using SonarLint.VisualStudio.ConnectedMode.Hotspots; -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents; -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Issue; -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.QualityProfile; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; +using SonarLint.VisualStudio.SLCore.State; using Task = System.Threading.Tasks.Task; namespace SonarLint.VisualStudio.ConnectedMode @@ -38,16 +34,12 @@ namespace SonarLint.VisualStudio.ConnectedMode [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] [ProvideAutoLoad(BoundSolutionUIContext.GuidString, PackageAutoLoadFlags.BackgroundLoad)] [Guid("dd3427e0-7bb2-4a51-b00a-ddae2c32c7ef")] - public sealed class ConnectedModePackage : AsyncPackage + public sealed class ConnectedModePackage : AsyncPackage, IDisposable { - private SSESessionManager sseSessionManager; - private IIssueServerEventsListener issueServerEventsListener; - private IQualityProfileServerEventsListener qualityProfileServerEventsListener; - private BoundSolutionUpdateHandler boundSolutionUpdateHandler; - private TimedUpdateHandler timedUpdateHandler; private IHotspotDocumentClosedHandler hotspotDocumentClosedHandler; private IHotspotSolutionClosedHandler hotspotSolutionClosedHandler; private ILocalHotspotStoreMonitor hotspotStoreMonitor; + private ISlCoreGitChangeNotifier slCoreGitChangeNotifier; protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) { @@ -57,17 +49,8 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke var logger = componentModel.GetService(); logger.WriteLine(Resources.Package_Initializing); - - LoadServicesAndDoInitialUpdates(componentModel); - - issueServerEventsListener = componentModel.GetService(); - issueServerEventsListener.ListenAsync().Forget(); - - qualityProfileServerEventsListener = componentModel.GetService(); - qualityProfileServerEventsListener.ListenAsync().Forget(); - - boundSolutionUpdateHandler = componentModel.GetService(); - timedUpdateHandler = componentModel.GetService(); + slCoreGitChangeNotifier = componentModel.GetService(); + await slCoreGitChangeNotifier.InitializationProcessor.InitializeAsync(); hotspotDocumentClosedHandler = componentModel.GetService(); @@ -76,33 +59,11 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke hotspotStoreMonitor = componentModel.GetService(); await hotspotStoreMonitor.InitializeAsync(); - logger.WriteLine(Resources.Package_Initialized); - } - /// - /// Trigger an initial update of classes that need them. (These classes might have missed the initial solution binding - /// event from the ActiveSolutionBoundTracker) - /// See https://github.com/SonarSource/sonarlint-visualstudio/issues/3886 - /// - private void LoadServicesAndDoInitialUpdates(IComponentModel componentModel) - { - sseSessionManager = componentModel.GetService(); - sseSessionManager.CreateSessionIfInConnectedMode(); - var updater = componentModel.GetService(); - updater.UpdateAllServerSuppressionsAsync().Forget(); + logger.WriteLine(Resources.Package_Initialized); } - protected override void Dispose(bool disposing) - { - if (disposing) - { - sseSessionManager?.Dispose(); - issueServerEventsListener?.Dispose(); - boundSolutionUpdateHandler?.Dispose(); - timedUpdateHandler?.Dispose(); - } - - base.Dispose(disposing); - } + public void Dispose() => + slCoreGitChangeNotifier.Dispose(); } } diff --git a/src/ConnectedMode/IssueMatcher.cs b/src/ConnectedMode/IssueMatcher.cs deleted file mode 100644 index 2e5e768972..0000000000 --- a/src/ConnectedMode/IssueMatcher.cs +++ /dev/null @@ -1,123 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Generic; -using System.ComponentModel.Composition; -using System.Linq; -using SonarLint.VisualStudio.Core.Helpers; -using SonarLint.VisualStudio.Core.Suppressions; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode -{ - internal interface IIssueMatcher - { - /// - /// This method attempts to match with . - /// - /// There's a possibility of False Positive matches: in case false tail-match of file paths, as the project root is not taken into account, - /// or when line number matches but line hash doesn't. - /// - /// Local issue - /// Server issue - /// The best possible match based on rule id, file name (tail matched to the server path), line number and line hash (not checked when line numbers match) - bool IsLikelyMatch(IFilterableIssue issue, SonarQubeIssue serverIssue); - - /// - /// Returns the first likely matching issue. - /// - /// For this method to work correctly, all ; need to be from the same server file. - /// False Positives are possible, since <see cref="IsLikelyMatch"/> can return true for multiple issues in the same file and only the firs one is returned. - /// - /// Local issue - /// List of server issues from the same file - /// A matching server issue, if present in the list, or null - SonarQubeIssue GetFirstLikelyMatchFromSameFileOrNull(IFilterableIssue issue, IEnumerable serverIssuesFromSameFile); - } - - [Export(typeof(IIssueMatcher))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal class IssueMatcher : IIssueMatcher - { - public bool IsLikelyMatch(IFilterableIssue issue, SonarQubeIssue serverIssue) - { - return IsMatch(issue, serverIssue, true); - } - - public SonarQubeIssue GetFirstLikelyMatchFromSameFileOrNull(IFilterableIssue issue, IEnumerable serverIssuesFromSameFile) - { - return serverIssuesFromSameFile?.FirstOrDefault(serverIssue => IsMatch(issue, serverIssue, false)); - } - - private static bool IsMatch(IFilterableIssue issue, SonarQubeIssue serverIssue, bool checkFilePath) - { - // File-level issues (i.e. line = null) match if: - // 1. Same component, same file, same error code. - - // Non-file-level issues match if: - // 1. Same component, same file, same error code, same line hash // tolerant to line number changing - // 2. Same component, same file, same error code, same line // tolerant to code on the line changing e.g. var rename - - // File-level issues never match non-file-level issues. - - if (!StringComparer.OrdinalIgnoreCase.Equals(issue.RuleId, serverIssue.RuleId)) - { - return false; - } - - if (checkFilePath && !PathHelper.IsServerFileMatch(issue.FilePath, serverIssue.FilePath)) - { - return false; - } - - // file level issue - if (IsFileLevelServerIssue(serverIssue)) - { - return CheckLocalIssueIsFileLevel(issue); - } - - // Non-file level issue - - return issue.StartLine == serverIssue.TextRange?.StartLine || CompareHash(issue, serverIssue); - } - - private static bool CheckLocalIssueIsFileLevel(IFilterableIssue issue) - { - return !issue.StartLine.HasValue - || (issue is IFilterableRoslynIssue roslynIssue // We don't know the end of the issue location, so this is our best guess. - && roslynIssue.RoslynStartLine == 1 - && roslynIssue.RoslynStartColumn == 1); // This check relies on the fact that a roslyn file-level issue - // always starts at the beginning of the file - // and the fact that the rule can't be both file-level and not. - // See SuppressionChecker.IsSameLine for an example of a solution w/o false-positives - } - - private static bool IsFileLevelServerIssue(SonarQubeIssue serverIssue) - { - return serverIssue.TextRange == null; - } - - private static bool CompareHash(IFilterableIssue issue, SonarQubeIssue serverIssue) => - issue.LineHash != null - && serverIssue.Hash != null - && StringComparer.Ordinal.Equals(issue.LineHash, serverIssue.Hash); - } -} diff --git a/src/ConnectedMode/Migration/ConnectedModeMigration.cs b/src/ConnectedMode/Migration/ConnectedModeMigration.cs index 12b67be23e..d45b2de1dd 100644 --- a/src/ConnectedMode/Migration/ConnectedModeMigration.cs +++ b/src/ConnectedMode/Migration/ConnectedModeMigration.cs @@ -21,7 +21,6 @@ using System.ComponentModel.Composition; using SonarLint.VisualStudio.ConnectedMode.Binding; using SonarLint.VisualStudio.ConnectedMode.Shared; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; using SonarQube.Client; @@ -43,8 +42,7 @@ private sealed class ChangedFiles : List> private readonly IFileCleaner fileCleaner; private readonly IVsAwareFileSystem fileSystem; private readonly ISonarQubeService sonarQubeService; - private readonly IUnintrusiveBindingController unintrusiveBindingController; - private readonly IRoslynSuppressionUpdater roslynSuppressionUpdater; + private readonly IConfigurationPersister configurationPersister; private readonly ISharedBindingConfigProvider sharedBindingConfigProvider; private readonly ILogger logger; private readonly IThreadHandling threadHandling; @@ -62,8 +60,7 @@ public ConnectedModeMigration( IFileCleaner fileCleaner, IVsAwareFileSystem fileSystem, ISonarQubeService sonarQubeService, - IUnintrusiveBindingController unintrusiveBindingController, - IRoslynSuppressionUpdater roslynSuppressionUpdater, + IConfigurationPersister configurationPersister, ISharedBindingConfigProvider sharedBindingConfigProvider, ILogger logger, IThreadHandling threadHandling, @@ -76,8 +73,7 @@ public ConnectedModeMigration( this.fileCleaner = fileCleaner; this.fileSystem = fileSystem; this.sonarQubeService = sonarQubeService; - this.unintrusiveBindingController = unintrusiveBindingController; - this.roslynSuppressionUpdater = roslynSuppressionUpdater; + this.configurationPersister = configurationPersister; this.sharedBindingConfigProvider = sharedBindingConfigProvider; this.logger = logger; @@ -150,19 +146,13 @@ private async Task MigrateImplAsync( progress?.Report(new MigrationProgress(0, 1, "Creating new binding files ...", false)); logger.WriteLine(MigrationStrings.Process_ProcessingNewBinding); - var progressAdapter = new FixedStepsProgressToMigrationProgressAdapter(progress); var serverConnection = GetServerConnectionWithMigration(oldBinding); - await unintrusiveBindingController.BindAsync(oldBinding.FromBoundSonarQubeProject(await solutionInfoProvider.GetSolutionNameAsync(), serverConnection), - progressAdapter, - token); + configurationPersister.Persist(oldBinding.FromBoundSonarQubeProject(await solutionInfoProvider.GetSolutionNameAsync(), serverConnection)); // Now make all of the files changes required to remove the legacy settings // i.e. update project files and delete .sonarlint folder await MakeLegacyFileChangesAsync(legacySettings, changedFiles, progress, token); - // Trigger a re-fetch of suppressions so the Roslyn settings are updated. - await roslynSuppressionUpdater.UpdateAllServerSuppressionsAsync(); - if (shareBinding) { progress?.Report(new MigrationProgress(0, 1, "Saving Shared Binding Config...", false)); diff --git a/src/ConnectedMode/Persistence/BindingJsonModel.cs b/src/ConnectedMode/Persistence/BindingJsonModel.cs index 30da22b8a0..be86a25030 100644 --- a/src/ConnectedMode/Persistence/BindingJsonModel.cs +++ b/src/ConnectedMode/Persistence/BindingJsonModel.cs @@ -36,5 +36,4 @@ internal class BindingJsonModel public SonarQubeOrganization Organization { get; set; } // left here for backward compatibility reasons public string ProjectKey { get; set; } public string ProjectName { get; set; } // left here for backward compatibility reasons - public Dictionary Profiles { get; set; } } diff --git a/src/ConnectedMode/Persistence/BindingJsonModelConverter.cs b/src/ConnectedMode/Persistence/BindingJsonModelConverter.cs index 01168db482..858e6965df 100644 --- a/src/ConnectedMode/Persistence/BindingJsonModelConverter.cs +++ b/src/ConnectedMode/Persistence/BindingJsonModelConverter.cs @@ -39,14 +39,13 @@ internal interface IBindingJsonModelConverter internal class BindingJsonModelConverter : IBindingJsonModelConverter { public BoundServerProject ConvertFromModel(BindingJsonModel bindingJsonModel, ServerConnection connection, string localBindingKey) => - new(localBindingKey, bindingJsonModel.ProjectKey, connection) { Profiles = bindingJsonModel.Profiles }; + new(localBindingKey, bindingJsonModel.ProjectKey, connection); public BindingJsonModel ConvertToModel(BoundServerProject binding) => new() { ProjectKey = binding.ServerProjectKey, ServerConnectionId = binding.ServerConnection.Id, - Profiles = binding.Profiles, // for compatibility reasons: ServerUri = binding.ServerConnection.ServerUri, Organization = binding.ServerConnection is ServerConnection.SonarCloud sonarCloudConnection @@ -59,5 +58,5 @@ public BoundSonarQubeProject ConvertFromModelToLegacy(BindingJsonModel bindingJs bindingJsonModel.ProjectKey, bindingJsonModel.ProjectName, credentials, - bindingJsonModel.Organization) { Profiles = bindingJsonModel.Profiles }; + bindingJsonModel.Organization); } diff --git a/src/ConnectedMode/Persistence/SolutionBindingRepository.cs b/src/ConnectedMode/Persistence/SolutionBindingRepository.cs index dd9864229e..9402fe8f34 100644 --- a/src/ConnectedMode/Persistence/SolutionBindingRepository.cs +++ b/src/ConnectedMode/Persistence/SolutionBindingRepository.cs @@ -145,20 +145,8 @@ public IEnumerable List() } } - private BindingJsonModel ReadBindingFile(string configFilePath) - { - var bound = solutionBindingFileLoader.Load(configFilePath); - - if (bound is null) - { - return null; - } - - Debug.Assert(!bound.Profiles?.ContainsKey(Language.Unknown) ?? true, - "Not expecting the deserialized binding config to contain the profile for an unknown language"); - - return bound; - } + private BindingJsonModel ReadBindingFile(string configFilePath) => + solutionBindingFileLoader.Load(configFilePath); private BoundServerProject Convert(BindingJsonModel bindingJsonModel, string configFilePath) => bindingJsonModel is not null && serverConnectionsRepository.TryGet(bindingJsonModel.ServerConnectionId, out var connection) diff --git a/src/ConnectedMode/ProjectRootCalculator.cs b/src/ConnectedMode/ProjectRootCalculator.cs deleted file mode 100644 index 78171f6277..0000000000 --- a/src/ConnectedMode/ProjectRootCalculator.cs +++ /dev/null @@ -1,69 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.Core.Helpers; -using SonarQube.Client; - -namespace SonarLint.VisualStudio.ConnectedMode -{ - internal interface IProjectRootCalculator - { - Task CalculateBasedOnLocalPathAsync(string localPath, CancellationToken token); - } - - [Export(typeof(IProjectRootCalculator))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal class ProjectRootCalculator : IProjectRootCalculator - { - private readonly ISonarQubeService sonarQubeService; - private readonly IActiveSolutionBoundTracker activeSolutionBoundTracker; - private readonly IStatefulServerBranchProvider branchProvider; - - [ImportingConstructor] - public ProjectRootCalculator(ISonarQubeService sonarQubeService, IActiveSolutionBoundTracker activeSolutionBoundTracker, IStatefulServerBranchProvider branchProvider) - { - this.sonarQubeService = sonarQubeService; - this.activeSolutionBoundTracker = activeSolutionBoundTracker; - this.branchProvider = branchProvider; - } - - public async Task CalculateBasedOnLocalPathAsync(string localPath, CancellationToken token) - { - var bindingConfiguration = activeSolutionBoundTracker.CurrentConfiguration; - - if (bindingConfiguration.Mode == SonarLintMode.Standalone) - { - return null; - } - - return PathHelper.CalculateServerRoot(localPath, - await sonarQubeService.SearchFilesByNameAsync(bindingConfiguration.Project.ServerProjectKey, - await branchProvider.GetServerBranchNameAsync(token), - Path.GetFileName(localPath), - token)); - } - } -} diff --git a/src/ConnectedMode/QualityProfiles/OutOfDateQualityProfileFinder.cs b/src/ConnectedMode/QualityProfiles/OutOfDateQualityProfileFinder.cs deleted file mode 100644 index 30f6f2f9e6..0000000000 --- a/src/ConnectedMode/QualityProfiles/OutOfDateQualityProfileFinder.cs +++ /dev/null @@ -1,85 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarQube.Client; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode.QualityProfiles -{ - internal interface IOutOfDateQualityProfileFinder - { - /// - /// Gives the list of outdated quality profiles based on the existing ones from - /// - Task> GetAsync( - BoundServerProject sonarQubeProject, - CancellationToken cancellationToken); - } - - [Export(typeof(IOutOfDateQualityProfileFinder))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal class OutOfDateQualityProfileFinder : IOutOfDateQualityProfileFinder - { - private readonly ISonarQubeService sonarQubeService; - private readonly ILanguageProvider languageProvider; - - [ImportingConstructor] - public OutOfDateQualityProfileFinder(ISonarQubeService sonarQubeService, ILanguageProvider languageProvider) - { - this.sonarQubeService = sonarQubeService; - this.languageProvider = languageProvider; - } - - public async Task> GetAsync( - BoundServerProject sonarQubeProject, - CancellationToken cancellationToken) - { - var sonarQubeQualityProfiles = - await sonarQubeService.GetAllQualityProfilesAsync(sonarQubeProject.ServerProjectKey, - (sonarQubeProject.ServerConnection as ServerConnection.SonarCloud)?.OrganizationKey, - cancellationToken); - - return sonarQubeQualityProfiles - .Select(serverQualityProfile => - (language: languageProvider.GetLanguageFromLanguageKey(serverQualityProfile.Language), - qualityProfile: serverQualityProfile)) - .Where(languageAndQp => - IsLocalQPOutOfDate(sonarQubeProject, languageAndQp.language, languageAndQp.qualityProfile)) - .ToArray(); - } - - private static bool IsLocalQPOutOfDate( - BoundServerProject sonarQubeProject, - Language language, - SonarQubeQualityProfile serverQualityProfile) - { - if (language == null || !sonarQubeProject.Profiles.TryGetValue(language, out var localQualityProfile)) - { - return false; - } - - return !serverQualityProfile.Key.Equals(localQualityProfile.ProfileKey) - || serverQualityProfile.TimeStamp > localQualityProfile.ProfileTimestamp; - } - } -} diff --git a/src/ConnectedMode/QualityProfiles/QualityProfileUpdater.cs b/src/ConnectedMode/QualityProfiles/QualityProfileUpdater.cs deleted file mode 100644 index 79c2dc835e..0000000000 --- a/src/ConnectedMode/QualityProfiles/QualityProfileUpdater.cs +++ /dev/null @@ -1,79 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using SonarLint.VisualStudio.ConnectedMode.Helpers; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using Task = System.Threading.Tasks.Task; - -namespace SonarLint.VisualStudio.ConnectedMode.QualityProfiles -{ - internal interface IQualityProfileUpdater - { - /// - /// When in Connected Mode, ensures that all of the Quality Profiles are up to date - /// - Task UpdateAsync(); - } - - [Export(typeof(IQualityProfileUpdater))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal sealed class QualityProfileUpdater : IQualityProfileUpdater, IDisposable - { - private readonly IConfigurationProvider configProvider; - private readonly IQualityProfileDownloader qualityProfileDownloader; - private readonly ICancellableActionRunner runner; - private readonly ILogger logger; - - [ImportingConstructor] - public QualityProfileUpdater(IConfigurationProvider configProvider, - IQualityProfileDownloader qualityProfileDownloader, - ICancellableActionRunner runner, - ILogger logger) - { - this.configProvider = configProvider; - this.qualityProfileDownloader = qualityProfileDownloader; - this.runner = runner; - this.logger = logger; - } - - public async Task UpdateAsync() - { - var config = configProvider.GetConfiguration(); - if (config.Mode != SonarLintMode.Connected) - { - logger.LogVerbose($"[{nameof(QualityProfileUpdater)}] Skipping Quality Profile update. Solution is not bound. Mode: {config.Mode}"); - return; - } - - try - { - await runner.RunAsync(async token => await qualityProfileDownloader.UpdateAsync(config.Project, null, token)); - } - catch (Exception e) when (e is OperationCanceledException || e is InvalidOperationException) - { - logger.LogVerbose($"[{nameof(QualityProfileUpdater)}] {e}"); - } - } - - public void Dispose() => runner.Dispose(); - } -} diff --git a/src/ConnectedMode/QualityProfiles/QualityProfilesStrings.Designer.cs b/src/ConnectedMode/QualityProfiles/QualityProfilesStrings.Designer.cs deleted file mode 100644 index f087ece6df..0000000000 --- a/src/ConnectedMode/QualityProfiles/QualityProfilesStrings.Designer.cs +++ /dev/null @@ -1,135 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace SonarLint.VisualStudio.ConnectedMode.QualityProfiles { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class QualityProfilesStrings { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal QualityProfilesStrings() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SonarLint.VisualStudio.ConnectedMode.QualityProfiles.QualityProfilesStrings", typeof(QualityProfilesStrings).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Downloading quality profile: {0}. - /// - internal static string DownloadingQualityProfileProgressMessage { - get { - return ResourceManager.GetString("DownloadingQualityProfileProgressMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to All quality profiles are up to date. - /// - internal static string DownloadingQualityProfilesNotNeeded { - get { - return ResourceManager.GetString("DownloadingQualityProfilesNotNeeded", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Failed to create the configuration for language {0}. - /// - internal static string FailedToCreateBindingConfigForLanguage { - get { - return ResourceManager.GetString("FailedToCreateBindingConfigForLanguage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Number of out of date Quality Profiles: {0}. - /// - internal static string OutOfDateQPsFound { - get { - return ResourceManager.GetString("OutOfDateQPsFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to [ConnectedMode/QualityProfiles] . - /// - internal static string QPMessagePrefix { - get { - return ResourceManager.GetString("QPMessagePrefix", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Successfully downloaded quality profile. Name: '{0}', Key: '{1}', Language: '{2}'. - /// - internal static string QualityProfileDownloadSuccessfulMessageFormat { - get { - return ResourceManager.GetString("QualityProfileDownloadSuccessfulMessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0}. - /// - internal static string SubTextPaddingFormat { - get { - return ResourceManager.GetString("SubTextPaddingFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Updating quality profiles.... - /// - internal static string UpdatingQualityProfiles { - get { - return ResourceManager.GetString("UpdatingQualityProfiles", resourceCulture); - } - } - } -} diff --git a/src/ConnectedMode/QualityProfiles/RoslynQualityProfileDownloader.cs b/src/ConnectedMode/QualityProfiles/RoslynQualityProfileDownloader.cs deleted file mode 100644 index 0841d4937f..0000000000 --- a/src/ConnectedMode/QualityProfiles/RoslynQualityProfileDownloader.cs +++ /dev/null @@ -1,131 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using SonarLint.VisualStudio.ConnectedMode.Binding; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode.QualityProfiles -{ - internal interface IQualityProfileDownloader - { - /// - /// Ensures that the Quality Profiles for all supported languages are to date - /// - /// true if there were changes updated, false if everything is up to date - /// If binding failed for one of the languages - Task UpdateAsync(BoundServerProject boundProject, IProgress progress, CancellationToken cancellationToken); - } - - [Export(typeof(IQualityProfileDownloader))] - [PartCreationPolicy(CreationPolicy.Shared)] - [method: ImportingConstructor] - internal class RoslynQualityProfileDownloader( - IBindingConfigProvider bindingConfigProvider, - IConfigurationPersister configurationPersister, - IOutOfDateQualityProfileFinder outOfDateQualityProfileFinder, - ILogger logger, - ILanguageProvider languageProvider) - : IQualityProfileDownloader - { - private readonly IEnumerable languagesToBind = languageProvider.RoslynLanguages; - - public async Task UpdateAsync( - BoundServerProject boundProject, - IProgress progress, - CancellationToken cancellationToken) - { - var isChanged = false; - - LogWithBindingPrefix(QualityProfilesStrings.UpdatingQualityProfiles); - - EnsureProfilesExistForAllSupportedLanguages(boundProject); - - var outOfDateProfiles = await outOfDateQualityProfileFinder.GetAsync(boundProject, cancellationToken); - - int currentLanguage = 0; - var totalLanguages = outOfDateProfiles.Count; - - LogWithBindingPrefix(string.Format(QualityProfilesStrings.OutOfDateQPsFound, totalLanguages)); - - foreach (var (language, qualityProfileInfo) in outOfDateProfiles) - { - if (!languagesToBind.Contains(language)) - { - continue; - } - currentLanguage++; - - var progressMessage = string.Format(QualityProfilesStrings.DownloadingQualityProfileProgressMessage, language.Name); - progress?.Report(new FixedStepsProgress(progressMessage, currentLanguage, totalLanguages)); - - UpdateProfile(boundProject, language, qualityProfileInfo); - - // (1) Save the top-level binding.config file that contains the QP timestamp - var bindingConfiguration = configurationPersister.Persist(boundProject); - - // (2) Save the language-specific rules config. - // Note: we've already saved the updated timestamp for the current language in the binding.config file at step (1) above - // If there is an error between (1) and (2) then the timestamp in the binding.config will show this language - // is up to date when it is not. - await bindingConfigProvider.SaveConfigurationAsync(qualityProfileInfo, language, bindingConfiguration, cancellationToken); - isChanged = true; - - LogWithBindingPrefix(string.Format(BindingStrings.SubTextPaddingFormat, - string.Format(QualityProfilesStrings.QualityProfileDownloadSuccessfulMessageFormat, qualityProfileInfo.Name, qualityProfileInfo.Key, language.Name))); - } - - if (!isChanged) - { - LogWithBindingPrefix(string.Format(QualityProfilesStrings.SubTextPaddingFormat, QualityProfilesStrings.DownloadingQualityProfilesNotNeeded)); - } - - return isChanged; - } - - /// - /// Ensures that the bound project has a profile entry for every supported language - /// - /// If we add support for new language in the future, this method will make sure it's - /// Quality Profile is fetched next time an update is triggered - private void EnsureProfilesExistForAllSupportedLanguages(BoundServerProject boundProject) - { - if (boundProject.Profiles == null) - { - boundProject.Profiles = new Dictionary(); - } - - foreach (var language in languagesToBind) - { - if (!boundProject.Profiles.ContainsKey(language)) - { - boundProject.Profiles[language] = new ApplicableQualityProfile { ProfileKey = null, ProfileTimestamp = DateTime.MinValue, }; - } - } - } - - private static void UpdateProfile(BoundServerProject boundSonarQubeProject, Language language, SonarQubeQualityProfile serverProfile) => - boundSonarQubeProject.Profiles[language] = new ApplicableQualityProfile { ProfileKey = serverProfile.Key, ProfileTimestamp = serverProfile.TimeStamp }; - - private void LogWithBindingPrefix(string text) => logger.WriteLine(QualityProfilesStrings.QPMessagePrefix + text); - } -} diff --git a/src/ConnectedMode/Resources.Designer.cs b/src/ConnectedMode/Resources.Designer.cs index 3028bdd85a..6c2029fd8f 100644 --- a/src/ConnectedMode/Resources.Designer.cs +++ b/src/ConnectedMode/Resources.Designer.cs @@ -1,7 +1,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -601,16 +600,7 @@ internal static string SharedBindingConfigProvider_SharedFolderNotFound { } /// - /// Looks up a localized string similar to [ConnectedMode/BranchMapping] Binding changed -> cache cleared. - /// - internal static string StatefulBranchProvider_BindingChanged { - get { - return ResourceManager.GetString("StatefulBranchProvider_BindingChanged", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to [ConnectedMode/BranchMapping] Binding updated -> cache cleared. + /// Looks up a localized string similar to Git HEAD changed, updating SLCore. /// internal static string StatefulBranchProvider_BindingUpdated { get { @@ -618,33 +608,6 @@ internal static string StatefulBranchProvider_BindingUpdated { } } - /// - /// Looks up a localized string similar to [ConnectedMode/BranchMapping] Cache hit. - /// - internal static string StatefulBranchProvider_CacheHit { - get { - return ResourceManager.GetString("StatefulBranchProvider_CacheHit", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to [ConnectedMode/BranchMapping] Cache miss. Recalculating server branch mapping.... - /// - internal static string StatefulBranchProvider_CacheMiss { - get { - return ResourceManager.GetString("StatefulBranchProvider_CacheMiss", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to [ConnectedMode/BranchMapping] Closest Sonar server branch: {0}. - /// - internal static string StatefulBranchProvider_ReturnValue { - get { - return ResourceManager.GetString("StatefulBranchProvider_ReturnValue", resourceCulture); - } - } - /// /// Looks up a localized string similar to Finished fetching suppressions. All issues suppressed: {0}. /// diff --git a/src/ConnectedMode/Resources.resx b/src/ConnectedMode/Resources.resx index 74bccefde2..0ba5bda1fd 100644 --- a/src/ConnectedMode/Resources.resx +++ b/src/ConnectedMode/Resources.resx @@ -175,20 +175,8 @@ [ConnectedMode] Initializing package... - - [ConnectedMode/BranchMapping] Binding changed -> cache cleared - - [ConnectedMode/BranchMapping] Binding updated -> cache cleared - - - [ConnectedMode/BranchMapping] Cache hit - - - [ConnectedMode/BranchMapping] Cache miss. Recalculating server branch mapping... - - - [ConnectedMode/BranchMapping] Closest Sonar server branch: {0} + Git HEAD changed, updating SLCore [ActionRunner] Cancelling current operation... diff --git a/src/ConnectedMode/ServerBranchProvider.cs b/src/ConnectedMode/ServerBranchProvider.cs index 7ed9769548..f8a922fded 100644 --- a/src/ConnectedMode/ServerBranchProvider.cs +++ b/src/ConnectedMode/ServerBranchProvider.cs @@ -19,100 +19,92 @@ */ using System.ComponentModel.Composition; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using LibGit2Sharp; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; -using SonarQube.Client; +using SonarLint.VisualStudio.SLCore.Listener.Branch; -namespace SonarLint.VisualStudio.ConnectedMode +namespace SonarLint.VisualStudio.ConnectedMode; + +[Export(typeof(IServerBranchProvider))] +internal class ServerBranchProvider : IServerBranchProvider { - [Export(typeof(IServerBranchProvider))] - internal class ServerBranchProvider : IServerBranchProvider + /// + /// Factory method to create and return an instance. + /// + /// Only used for testing + internal delegate IRepository CreateRepositoryObject(string repoRootPath); + + private readonly IActiveSolutionBoundTracker activeSolutionBoundTracker; + private readonly IGitWorkspaceService gitWorkspaceService; + private readonly IBranchMatcher branchMatcher; + private readonly ILogger logger; + private readonly CreateRepositoryObject createRepo; + + [ImportingConstructor] + public ServerBranchProvider( + IActiveSolutionBoundTracker activeSolutionBoundTracker, + IGitWorkspaceService gitWorkspaceService, + IBranchMatcher branchMatcher, + ILogger logger) + : this(activeSolutionBoundTracker, gitWorkspaceService, branchMatcher, logger, DoCreateRepo) { - /// - /// Factory method to create and return an instance. - /// - /// Only used for testing - internal delegate IRepository CreateRepositoryObject(string repoRootPath); - - private readonly IConfigurationProvider configurationProvider; - private readonly IGitWorkspaceService gitWorkspaceService; - private readonly ISonarQubeService sonarQubeService; - private readonly IBranchMatcher branchMatcher; - private readonly ILogger logger; - private readonly CreateRepositoryObject createRepo; - - [ImportingConstructor] - public ServerBranchProvider(IConfigurationProvider configurationProvider, - IGitWorkspaceService gitWorkspaceService, - ISonarQubeService sonarQubeService, - IBranchMatcher branchMatcher, - ILogger logger) - : this(configurationProvider, gitWorkspaceService, sonarQubeService, branchMatcher, logger, DoCreateRepo) - { - } - - internal /* for testing */ ServerBranchProvider(IConfigurationProvider configurationProvider, - IGitWorkspaceService gitWorkspaceService, - ISonarQubeService sonarQubeService, - IBranchMatcher branchMatcher, - ILogger logger, - CreateRepositoryObject createRepo) - { - this.configurationProvider = configurationProvider; - this.gitWorkspaceService = gitWorkspaceService; - this.sonarQubeService = sonarQubeService; - this.branchMatcher = branchMatcher; - this.logger = logger; - this.createRepo = createRepo; - } - - public async Task GetServerBranchNameAsync(CancellationToken token) - { - var config = configurationProvider.GetConfiguration(); + } - if (config.Mode == SonarLintMode.Standalone) - { - logger.LogVerbose(Resources.BranchProvider_NotInConnectedMode); - return null; - } + internal /* for testing */ ServerBranchProvider( + IActiveSolutionBoundTracker activeSolutionBoundTracker, + IGitWorkspaceService gitWorkspaceService, + IBranchMatcher branchMatcher, + ILogger logger, + CreateRepositoryObject createRepo) + { + this.activeSolutionBoundTracker = activeSolutionBoundTracker; + this.gitWorkspaceService = gitWorkspaceService; + this.branchMatcher = branchMatcher; + this.logger = logger; + this.createRepo = createRepo; + } - var matchingBranchName = await CalculateMatchingBranchAsync(config, token); + public string GetServerBranchName(List branches) + { + var config = activeSolutionBoundTracker.CurrentConfiguration; - if (matchingBranchName == null) - { - logger.LogVerbose(Resources.BranchProvider_FailedToCalculateMatchingBranch); + if (config.Mode == SonarLintMode.Standalone) + { + logger.LogVerbose(Resources.BranchProvider_NotInConnectedMode); + return null; + } - var remoteBranches = await sonarQubeService.GetProjectBranchesAsync(config.Project.ServerProjectKey, token); - matchingBranchName = remoteBranches.First(rb => rb.IsMain).Name; - } + var matchingBranchName = CalculateMatchingBranch(config, branches); - Debug.Assert(matchingBranchName != null); + if (matchingBranchName == null) + { + logger.LogVerbose(Resources.BranchProvider_FailedToCalculateMatchingBranch); - logger.WriteLine(Resources.BranchProvider_MatchingServerBranchName, matchingBranchName); - return matchingBranchName; + matchingBranchName = branches.First(rb => rb.IsMain).Name; } - private async Task CalculateMatchingBranchAsync(BindingConfiguration config, CancellationToken token) - { - var gitRepoRoot = gitWorkspaceService.GetRepoRoot(); + Debug.Assert(matchingBranchName != null); - if (gitRepoRoot == null) - { - logger.LogVerbose(Resources.BranchProvider_CouldNotDetectGitRepo); - return null; - } + logger.WriteLine(Resources.BranchProvider_MatchingServerBranchName, matchingBranchName); + return matchingBranchName; + } - var repo = createRepo(gitRepoRoot); - var branchName = await branchMatcher.GetMatchingBranch(config.Project.ServerProjectKey, repo, token); + private string CalculateMatchingBranch(BindingConfiguration config, List branches) + { + var gitRepoRoot = gitWorkspaceService.GetRepoRoot(); - return branchName; + if (gitRepoRoot == null) + { + logger.LogVerbose(Resources.BranchProvider_CouldNotDetectGitRepo); + return null; } - private static IRepository DoCreateRepo(string repoRootPath) => new Repository(repoRootPath); + var repo = createRepo(gitRepoRoot); + var branchName = branchMatcher.GetMatchingBranch(config.Project.ServerProjectKey, repo, branches); + + return branchName; } + + private static IRepository DoCreateRepo(string repoRootPath) => new Repository(repoRootPath); } diff --git a/src/ConnectedMode/ServerIssueFinder.cs b/src/ConnectedMode/ServerIssueFinder.cs deleted file mode 100644 index 6e44515185..0000000000 --- a/src/ConnectedMode/ServerIssueFinder.cs +++ /dev/null @@ -1,95 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.ComponentModel.Composition; -using System.Threading; -using System.Threading.Tasks; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.Core.Suppressions; -using SonarQube.Client; -using SonarQube.Client.Helpers; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode -{ - public interface IServerIssueFinder - { - Task FindServerIssueAsync(IFilterableIssue localIssue, CancellationToken token); - } - - [Export(typeof(IServerIssueFinder))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal class ServerIssueFinder : IServerIssueFinder - { - private readonly IProjectRootCalculator projectRootCalculator; - private readonly IIssueMatcher issueMatcher; - private readonly IActiveSolutionBoundTracker activeSolutionBoundTracker; - private readonly IStatefulServerBranchProvider serverBranchProvider; - private readonly ISonarQubeService sonarQubeService; - private readonly IThreadHandling threadHandling; - - [ImportingConstructor] - public ServerIssueFinder(IProjectRootCalculator projectRootCalculator, IIssueMatcher issueMatcher, - IActiveSolutionBoundTracker activeSolutionBoundTracker, IStatefulServerBranchProvider serverBranchProvider, - ISonarQubeService sonarQubeService, IThreadHandling threadHandling) - { - this.projectRootCalculator = projectRootCalculator; - this.issueMatcher = issueMatcher; - this.activeSolutionBoundTracker = activeSolutionBoundTracker; - this.serverBranchProvider = serverBranchProvider; - this.sonarQubeService = sonarQubeService; - this.threadHandling = threadHandling; - } - - public async Task FindServerIssueAsync(IFilterableIssue localIssue, CancellationToken token) - { - threadHandling.ThrowIfOnUIThread(); - - var bindingConfiguration = activeSolutionBoundTracker.CurrentConfiguration; - - if (bindingConfiguration.Mode == SonarLintMode.Standalone) - { - return null; - } - - var projectRoot = await projectRootCalculator.CalculateBasedOnLocalPathAsync(localIssue.FilePath, token); - - if (projectRoot == null) - { - return null; - } - - var componentKey = ComponentKeyGenerator.GetComponentKey(localIssue.FilePath, - projectRoot, - bindingConfiguration.Project.ServerProjectKey); - - var serverIssues = await sonarQubeService.GetIssuesForComponentAsync( - bindingConfiguration.Project.ServerProjectKey, - await serverBranchProvider.GetServerBranchNameAsync(token), - componentKey, - localIssue.RuleId, - token); - - return issueMatcher.GetFirstLikelyMatchFromSameFileOrNull(localIssue, serverIssues); - } - } -} diff --git a/src/ConnectedMode/ServerQueryInfoProvider.cs b/src/ConnectedMode/ServerQueryInfoProvider.cs deleted file mode 100644 index 8947b8b4a2..0000000000 --- a/src/ConnectedMode/ServerQueryInfoProvider.cs +++ /dev/null @@ -1,70 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using System.Threading; -using System.Threading.Tasks; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; - -namespace SonarLint.VisualStudio.ConnectedMode -{ - /// - /// Returns the information necessary to make various queries to the Sonar server - /// - /// Facade to simplify classes that need to make branch-aware calls. - /// The projectKey and server branch are provided by different interfaces. This interface - /// simplifies getting the information. - internal interface IServerQueryInfoProvider - { - /// - /// Returns the projectKey and branch name for the current bound solution, or - /// (null, null) if the solution is not bound - /// - Task<(string projectKey, string branchName)> GetProjectKeyAndBranchAsync(CancellationToken token); - } - - [Export(typeof(IServerQueryInfoProvider))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal class ServerQueryInfoProvider : IServerQueryInfoProvider - { - private readonly IConfigurationProvider configurationProvider; - private readonly IStatefulServerBranchProvider serverBranchProvider; - - [ImportingConstructor] - public ServerQueryInfoProvider(IConfigurationProvider configurationProvider, IStatefulServerBranchProvider serverBranchProvider) - { - this.configurationProvider = configurationProvider; - this.serverBranchProvider = serverBranchProvider; - } - - public async Task<(string projectKey, string branchName)> GetProjectKeyAndBranchAsync(CancellationToken token) - { - var config = configurationProvider.GetConfiguration(); - if (config.Mode == SonarLintMode.Standalone) - { - return (null, null); - } - - var branchName = await serverBranchProvider.GetServerBranchNameAsync(token); - return (config.Project.ServerProjectKey, branchName); - } - } -} diff --git a/src/ConnectedMode/ServerSentEvents/Issue/IIssueServerEventSource.cs b/src/ConnectedMode/ServerSentEvents/Issue/IIssueServerEventSource.cs deleted file mode 100644 index dca9008879..0000000000 --- a/src/ConnectedMode/ServerSentEvents/Issue/IIssueServerEventSource.cs +++ /dev/null @@ -1,28 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.Core.ServerSentEvents; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Issue -{ - /// - internal interface IIssueServerEventSource : IServerSentEventSource { } -} diff --git a/src/ConnectedMode/ServerSentEvents/Issue/IIssueServerEventSourcePublisher.cs b/src/ConnectedMode/ServerSentEvents/Issue/IIssueServerEventSourcePublisher.cs deleted file mode 100644 index cf0f3c07d9..0000000000 --- a/src/ConnectedMode/ServerSentEvents/Issue/IIssueServerEventSourcePublisher.cs +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.SonarQubeClient; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Issue -{ - /// - public interface IIssueServerEventSourcePublisher : IServerSentEventSourcePublisher - { - } -} diff --git a/src/ConnectedMode/ServerSentEvents/Issue/IServerSentEventSource.cs b/src/ConnectedMode/ServerSentEvents/Issue/IServerSentEventSource.cs deleted file mode 100644 index a2c59687c2..0000000000 --- a/src/ConnectedMode/ServerSentEvents/Issue/IServerSentEventSource.cs +++ /dev/null @@ -1,49 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Issue -{ - /// - /// Source for the server sent events about certain server side changes to the SQ/SC project - /// - /// This interface is not intended to be thread safe - /// Server sent event type inherited from - public interface IServerSentEventSource where T : class, IServerEvent - { - /// - /// Method that is used to await for the next server sent event . - /// - /// Does not throw, always returns null after it's disposed - /// - /// Task which result is: - /// - /// - /// Next server event in queue - /// - /// - /// Or null, when the channel has been Disposed - /// - /// - /// - Task GetNextEventOrNullAsync(); - } -} diff --git a/src/ConnectedMode/ServerSentEvents/Issue/IServerSentEventSourcePublisher.cs b/src/ConnectedMode/ServerSentEvents/Issue/IServerSentEventSourcePublisher.cs deleted file mode 100644 index 5203ad7e56..0000000000 --- a/src/ConnectedMode/ServerSentEvents/Issue/IServerSentEventSourcePublisher.cs +++ /dev/null @@ -1,41 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarLint.VisualStudio.ConnectedMode.SonarQubeClient -{ - /// - /// The publishing side for the - /// - /// This interface is not intended to be thread safe. - /// The only permitted type of multi threaded calling is calling Publish and Dispose concurrently, although it may result in - /// Server sent event type inherited from - public interface IServerSentEventSourcePublisher : IDisposable where T : class, IServerEvent - { - /// - /// Publishes the event to the consumer channel. - /// After the instance has been disposed. - /// - /// Server event () that needs to be delivered to the consumer - /// - void Publish(T serverEvent); - } -} diff --git a/src/ConnectedMode/ServerSentEvents/Issue/IssueServerEventsListener.cs b/src/ConnectedMode/ServerSentEvents/Issue/IssueServerEventsListener.cs deleted file mode 100644 index 48d2a2dc57..0000000000 --- a/src/ConnectedMode/ServerSentEvents/Issue/IssueServerEventsListener.cs +++ /dev/null @@ -1,116 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; -using SonarLint.VisualStudio.Core; - -namespace SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Issue -{ - /// - /// Handles coming from the server. - /// - internal interface IIssueServerEventsListener : IDisposable - { - Task ListenAsync(); - } - - [Export(typeof(IIssueServerEventsListener))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal sealed class IssueServerEventsListener : IIssueServerEventsListener - { - private readonly IIssueServerEventSource issueServerEventSource; - private readonly IRoslynSuppressionUpdater roslynSuppressionUpdater; - private readonly IThreadHandling threadHandling; - private readonly IStatefulServerBranchProvider branchProvider; - private readonly ILogger logger; - private readonly CancellationTokenSource cancellationTokenSource; - - [ImportingConstructor] - public IssueServerEventsListener( - IIssueServerEventSource issueServerEventSource, - IRoslynSuppressionUpdater roslynSuppressionUpdater, - IStatefulServerBranchProvider branchProvider, - IThreadHandling threadHandling, - ILogger logger) - { - this.issueServerEventSource = issueServerEventSource; - this.roslynSuppressionUpdater = roslynSuppressionUpdater; - this.branchProvider = branchProvider; - this.threadHandling = threadHandling; - this.logger = logger; - - cancellationTokenSource = new CancellationTokenSource(); - } - - public async Task ListenAsync() - { - await threadHandling.SwitchToBackgroundThread(); - - while (!cancellationTokenSource.IsCancellationRequested) - { - try - { - var issueServerEvent = await issueServerEventSource.GetNextEventOrNullAsync(); - - if (issueServerEvent == null) - { - // Will return null when issueServerEventSource is disposed - return; - } - - var serverBranch = await branchProvider.GetServerBranchNameAsync(cancellationTokenSource.Token); - - var issueKeysForCurrentBranch = issueServerEvent.BranchAndIssueKeys - .Where(x => x.BranchName.Equals(serverBranch)) - .Select(x => x.IssueKey) - .ToArray(); - - logger.LogVerbose(Resources.Suppression_IssueChangedEvent, issueServerEvent.IsResolved, string.Join(",", issueKeysForCurrentBranch)); - - if (issueKeysForCurrentBranch.Any()) - { - await roslynSuppressionUpdater.UpdateSuppressedIssuesAsync(issueServerEvent.IsResolved, issueKeysForCurrentBranch, cancellationTokenSource.Token); - } - - logger.LogVerbose(Resources.Suppression_IssueChangedEventFinished); - } - catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) - { - logger.LogVerbose($"[IssueServerEventsListener] Failed to handle issue event: {ex}"); - } - } - } - - private bool disposed; - - public void Dispose() - { - if (disposed) - { - return; - } - - cancellationTokenSource.Cancel(); - cancellationTokenSource.Dispose(); - disposed = true; - } - } -} diff --git a/src/ConnectedMode/ServerSentEvents/QualityProfile/IQualityProfileServerEventSource.cs b/src/ConnectedMode/ServerSentEvents/QualityProfile/IQualityProfileServerEventSource.cs deleted file mode 100644 index d9878eabfa..0000000000 --- a/src/ConnectedMode/ServerSentEvents/QualityProfile/IQualityProfileServerEventSource.cs +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Issue; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.QualityProfile -{ - /// - internal interface IQualityProfileServerEventSource : IServerSentEventSource - { - } -} diff --git a/src/ConnectedMode/ServerSentEvents/QualityProfile/IQualityProfileServerEventSourcePublisher.cs b/src/ConnectedMode/ServerSentEvents/QualityProfile/IQualityProfileServerEventSourcePublisher.cs deleted file mode 100644 index cd011e7dc8..0000000000 --- a/src/ConnectedMode/ServerSentEvents/QualityProfile/IQualityProfileServerEventSourcePublisher.cs +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.SonarQubeClient; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.QualityProfile -{ - /// - internal interface IQualityProfileServerEventSourcePublisher : IServerSentEventSourcePublisher - { - } -} diff --git a/src/ConnectedMode/ServerSentEvents/QualityProfile/QualityProfileServerEventsListener.cs b/src/ConnectedMode/ServerSentEvents/QualityProfile/QualityProfileServerEventsListener.cs deleted file mode 100644 index 46cdf1de5a..0000000000 --- a/src/ConnectedMode/ServerSentEvents/QualityProfile/QualityProfileServerEventsListener.cs +++ /dev/null @@ -1,64 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using System.Threading.Tasks; -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.Core; - -namespace SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.QualityProfile -{ - /// - /// Event listener for - /// - internal interface IQualityProfileServerEventsListener - { - Task ListenAsync(); - } - - [Export(typeof(IQualityProfileServerEventsListener))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal class QualityProfileServerEventsListener : IQualityProfileServerEventsListener - { - private readonly IQualityProfileServerEventSource eventSource; - private readonly IQualityProfileUpdater updater; - private readonly IThreadHandling threadHandling; - - [ImportingConstructor] - public QualityProfileServerEventsListener(IQualityProfileServerEventSource eventSource, IQualityProfileUpdater updater, IThreadHandling threadHandling) - { - this.eventSource = eventSource; - this.updater = updater; - this.threadHandling = threadHandling; - } - - public async Task ListenAsync() - { - await threadHandling.SwitchToBackgroundThread(); - - // when event source is disposed, it returns null - while (await eventSource.GetNextEventOrNullAsync() != null) - { - // the update is cancelled via it's own Dispose, managed elsewhere - await updater.UpdateAsync(); - } - } - } -} diff --git a/src/ConnectedMode/ServerSentEvents/SSESessionFactory.cs b/src/ConnectedMode/ServerSentEvents/SSESessionFactory.cs deleted file mode 100644 index 5558e6c90b..0000000000 --- a/src/ConnectedMode/ServerSentEvents/SSESessionFactory.cs +++ /dev/null @@ -1,212 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using Microsoft.VisualStudio.Threading; -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Issue; -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.QualityProfile; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.ServerSentEvents; -using SonarQube.Client; -using SonarQube.Client.Models.ServerSentEvents; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarLint.VisualStudio.ConnectedMode.ServerSentEvents; - -internal delegate Task OnSessionFailedAsync(ISSESession failedSession); - -/// -/// Factory for . Responsible for disposing EventSourcePublishers -/// -internal interface ISSESessionFactory : IDisposable -{ - ISSESession Create(string projectKey, OnSessionFailedAsync onSessionFailedCallback); -} - -/// -/// Represents the session entity, that is responsible for dealing with reader -/// and propagating events to correct topic event publishers -/// -internal interface ISSESession : IDisposable -{ - Task PumpAllAsync(); -} - -[Export(typeof(ISSESessionFactory))] -[PartCreationPolicy(CreationPolicy.Shared)] -internal sealed class SSESessionFactory : ISSESessionFactory -{ - private readonly ISonarQubeService sonarQubeClient; - private readonly IIssueServerEventSourcePublisher issueServerEventSourcePublisher; - private readonly IQualityProfileServerEventSourcePublisher qualityProfileServerEventSourcePublisher; - private readonly IThreadHandling threadHandling; - - private bool disposed; - private readonly ILogger logger; - - [ImportingConstructor] - public SSESessionFactory(ISonarQubeService sonarQubeClient, - IIssueServerEventSourcePublisher issueServerEventSourcePublisher, - IQualityProfileServerEventSourcePublisher qualityProfileServerEventSourcePublisher, - IThreadHandling threadHandling, - ILogger logger) - { - this.sonarQubeClient = sonarQubeClient; - this.issueServerEventSourcePublisher = issueServerEventSourcePublisher; - this.qualityProfileServerEventSourcePublisher = qualityProfileServerEventSourcePublisher; - this.threadHandling = threadHandling; - this.logger = logger; - } - - public ISSESession Create(string projectKey, OnSessionFailedAsync onSessionFailedCallback) - { - if (disposed) - { - throw new ObjectDisposedException(nameof(SSESessionFactory)); - } - - var session = new SSESession( - issueServerEventSourcePublisher, - qualityProfileServerEventSourcePublisher, - projectKey, - threadHandling, - sonarQubeClient, - onSessionFailedCallback, - logger); - - return session; - } - - public void Dispose() - { - if (disposed) - { - return; - } - - issueServerEventSourcePublisher.Dispose(); - qualityProfileServerEventSourcePublisher.Dispose(); - disposed = true; - } - - internal sealed class SSESession : ISSESession - { - private readonly IIssueServerEventSourcePublisher issueServerEventSourcePublisher; - private readonly IQualityProfileServerEventSourcePublisher qualityProfileServerEventSourcePublisher; - private readonly string projectKey; - private readonly IThreadHandling threadHandling; - private readonly ISonarQubeService sonarQubeService; - private readonly OnSessionFailedAsync onSessionFailedCallback; - private readonly ILogger logger; - private readonly CancellationTokenSource sessionTokenSource; - - private bool disposed; - - internal SSESession(IIssueServerEventSourcePublisher issueServerEventSourcePublisher, - IQualityProfileServerEventSourcePublisher qualityProfileServerEventSourcePublisher, - string projectKey, - IThreadHandling threadHandling, - ISonarQubeService sonarQubeService, - OnSessionFailedAsync onSessionFailedCallback, - ILogger logger) - { - this.issueServerEventSourcePublisher = issueServerEventSourcePublisher; - this.qualityProfileServerEventSourcePublisher = qualityProfileServerEventSourcePublisher; - this.projectKey = projectKey; - this.threadHandling = threadHandling; - this.sonarQubeService = sonarQubeService; - this.onSessionFailedCallback = onSessionFailedCallback; - this.logger = logger; - this.sessionTokenSource = new CancellationTokenSource(); - } - - public async Task PumpAllAsync() - { - if (disposed) - { - logger.LogVerbose("[SSESession] Session {0} is disposed", GetHashCode()); - throw new ObjectDisposedException(nameof(SSESession)); - } - - await threadHandling.SwitchToBackgroundThread(); - - var sseStreamReader = await sonarQubeService.CreateSSEStreamReader(projectKey, sessionTokenSource.Token); - - if (sseStreamReader == null) - { - logger.LogVerbose("[SSESession] Failed to create CreateSSEStreamReader"); - return; - } - - while (!sessionTokenSource.IsCancellationRequested) - { - try - { - var serverEvent = await sseStreamReader.ReadAsync(); - - if (serverEvent == null) - { - continue; - } - - logger.LogVerbose("[SSESession] Received server event: {0}", serverEvent.GetType()); - - switch (serverEvent) - { - case IIssueChangedServerEvent issueChangedServerEvent: - { - logger.LogVerbose("[SSESession] Publishing issue changed event..."); - issueServerEventSourcePublisher.Publish(issueChangedServerEvent); - break; - } - case IQualityProfileEvent qualityProfileEvent: - { - logger.LogVerbose("[SSESession] Publishing quality profile event..."); - qualityProfileServerEventSourcePublisher.Publish(qualityProfileEvent); - break; - } - } - } - catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) - { - logger.LogVerbose($"[SSESession] Failed to handle events: {ex}"); - onSessionFailedCallback(this).Forget(); - Dispose(); - return; - } - } - - logger.LogVerbose("[SSESession] Session stopped, session token was canceled"); - } - - public void Dispose() - { - logger.LogVerbose("[SSESession] Disposing session: {0}", GetHashCode()); - - if (disposed) - { - return; - } - - disposed = true; - sessionTokenSource.Cancel(); - } - } -} diff --git a/src/ConnectedMode/ServerSentEvents/SSESessionManager.cs b/src/ConnectedMode/ServerSentEvents/SSESessionManager.cs deleted file mode 100644 index 8ac771b4cb..0000000000 --- a/src/ConnectedMode/ServerSentEvents/SSESessionManager.cs +++ /dev/null @@ -1,134 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using Microsoft.VisualStudio.Threading; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; - -namespace SonarLint.VisualStudio.ConnectedMode.ServerSentEvents -{ - [Export(typeof(SSESessionManager))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal sealed class SSESessionManager : IDisposable - { - private const int DelayTimeBetweenRetriesInMilliseconds = 1000; - - private readonly object syncRoot = new object(); - private readonly IActiveSolutionBoundTracker activeSolutionBoundTracker; - private readonly ISSESessionFactory sseSessionFactory; - private readonly ILogger logger; - - private ISSESession currentSession; - - private bool disposed; - - [ImportingConstructor] - public SSESessionManager(IActiveSolutionBoundTracker activeSolutionBoundTracker, - ISSESessionFactory sseSessionFactory, - ILogger logger) - { - this.activeSolutionBoundTracker = activeSolutionBoundTracker; - this.sseSessionFactory = sseSessionFactory; - this.logger = logger; - - activeSolutionBoundTracker.SolutionBindingChanged += SolutionBindingChanged; - } - - public void Dispose() - { - if (disposed) - { - return; - } - - activeSolutionBoundTracker.SolutionBindingChanged -= SolutionBindingChanged; - sseSessionFactory.Dispose(); - EndCurrentSession(); - disposed = true; - } - - private void SolutionBindingChanged(object sender, ActiveSolutionBindingEventArgs activeSolutionBindingEventArgs) - { - CreateSessionIfInConnectedMode(activeSolutionBindingEventArgs.Configuration); - } - - /// - /// Creates a new session if in connected mode. If no binding configuration is provided the - /// ActiveSolutionBoundTracker.CurrentConfiguration will be used. - /// - public void CreateSessionIfInConnectedMode(BindingConfiguration bindingConfiguration = null) - { - if (bindingConfiguration == null) { bindingConfiguration = activeSolutionBoundTracker.CurrentConfiguration; } - - lock (syncRoot) - { - EndCurrentSession(); - - var isInConnectedMode = !bindingConfiguration.Equals(BindingConfiguration.Standalone); - - if (!isInConnectedMode) - { - logger.LogVerbose("[SSESessionManager] Not in connected mode"); - return; - } - - if (bindingConfiguration.Project.ServerConnection is ServerConnection.SonarCloud) - { - logger.LogVerbose("[SSESessionManager] Not available for the current server connection"); - return; - } - - logger.LogVerbose("[SSESessionManager] In connected mode, creating session..."); - - currentSession = sseSessionFactory.Create(bindingConfiguration.Project.ServerProjectKey, OnSessionFailedAsync); - - logger.LogVerbose("[SSESessionManager] Created session: {0}", currentSession.GetHashCode()); - - currentSession.PumpAllAsync().Forget(); - - logger.LogVerbose("[SSESessionManager] Session started"); - } - } - - private async Task OnSessionFailedAsync(ISSESession failedSession) - { - logger.LogVerbose("[SSESessionManager] Session failed: " + failedSession.GetHashCode()); - - await Task.Delay(DelayTimeBetweenRetriesInMilliseconds); - CreateSessionIfInConnectedMode(activeSolutionBoundTracker.CurrentConfiguration); - - logger.LogVerbose("[SSESessionManager] Finished handling session failure"); - } - - private void EndCurrentSession() - { - logger.LogVerbose("[SSESessionManager] Ending current session..."); - - lock (syncRoot) - { - logger.LogVerbose("[SSESessionManager] Disposing current session: {0}", currentSession?.GetHashCode()); - - currentSession?.Dispose(); - currentSession = null; - } - } - } -} diff --git a/src/ConnectedMode/ServerSentEvents/ServerEventChannel.cs b/src/ConnectedMode/ServerSentEvents/ServerEventChannel.cs deleted file mode 100644 index 841b05aaf9..0000000000 --- a/src/ConnectedMode/ServerSentEvents/ServerEventChannel.cs +++ /dev/null @@ -1,83 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Threading.Channels; -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Issue; -using SonarLint.VisualStudio.ConnectedMode.SonarQubeClient; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarLint.VisualStudio.Core.ServerSentEvents -{ - /// - /// Base class for channels of server events divided by topics - /// - /// - /// Even though and do not allow their own methods to be called from different threads, this class was designed to allow the calls to two different interfaces' methods to be made from different threads at the same time. - /// This means that calling from one thread and calling or from a different thread at the same time is allowed, while calling methods of the same interface from different threads is not. - /// NOTE: there's an exception to this rule that was made to simplify the implementation of the events pump that permits calling Dispose and Publish concurrently, but results in an exception. For more info see - /// - /// - public class ServerEventChannel : IServerSentEventSource, IServerSentEventSourcePublisher where T : class, IServerEvent - { - private bool disposed; - /// - /// Channel that is used for storing and awaiting new items - /// - private readonly Channel channel = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = true, SingleWriter = true }); - - public async Task GetNextEventOrNullAsync() - { - await channel.Reader.WaitToReadAsync().ConfigureAwait(false); - return channel.Reader.TryRead(out var item) - ? item - : null; - } - - public void Publish(T serverEvent) - { - // The behaviour for TryWrite depends, in some aspects, on the type of the channel we use.In particular, what we are interested in is what it returns before and after marking the channel as complete. - // Because we specifically create an unbound channel with the SingleReader = true, SingleWriter = true settings, we actually get a SingleConsumerUnboundedChannel instance that has the following behaviour: TryWrite always returns true before the channel is marked as complete, and always returns false afterwards. - // This behaviour allows us to be sure that the only way we can interpret the false result is that the channel was closed. - // This is valuable for us because the only other way of knowing that would be calling WriteAsync and catching the exception, as the ChannelReader.Completed task is only resolved when there's no more items in the channel, and that may not happen at the same time as we called ChannelWriter.Complete. - // So in short, the reason for this assumption was to simplify our channel wrapper code. - if (!channel.Writer.TryWrite(serverEvent ?? throw new ArgumentNullException(nameof(serverEvent)))) - { - throw new ObjectDisposedException(nameof(ServerEventChannel)); - } - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (disposed) - { - return; - } - - channel.Writer.Complete(); - disposed = true; - } - } -} diff --git a/src/ConnectedMode/SlCoreGitChangeNotifier.cs b/src/ConnectedMode/SlCoreGitChangeNotifier.cs new file mode 100644 index 0000000000..f13b92d653 --- /dev/null +++ b/src/ConnectedMode/SlCoreGitChangeNotifier.cs @@ -0,0 +1,110 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using Microsoft.VisualStudio.Threading; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.ConfigurationScope; +using SonarLint.VisualStudio.Core.Initialization; +using SonarLint.VisualStudio.SLCore; +using SonarLint.VisualStudio.SLCore.Core; +using SonarLint.VisualStudio.SLCore.Service.Branch; +using SonarLint.VisualStudio.SLCore.State; + +namespace SonarLint.VisualStudio.ConnectedMode; + +[Export(typeof(ISlCoreGitChangeNotifier))] +[PartCreationPolicy(CreationPolicy.Shared)] +internal sealed class SlCoreGitChangeNotifier : ISlCoreGitChangeNotifier +{ + private readonly IActiveConfigScopeTracker activeConfigScopeTracker; + private readonly ISLCoreServiceProvider serviceProvider; + private readonly IBoundSolutionGitMonitor gitMonitor; + private readonly ILogger logger; + private readonly IThreadHandling threadHandling; + private bool disposed; + + [ImportingConstructor] + public SlCoreGitChangeNotifier( + IActiveConfigScopeTracker activeConfigScopeTracker, + ISLCoreServiceProvider serviceProvider, + IBoundSolutionGitMonitor gitMonitor, + ILogger logger, + IThreadHandling threadHandling, + IInitializationProcessorFactory initializationProcessorFactory) + { + this.activeConfigScopeTracker = activeConfigScopeTracker; + this.serviceProvider = serviceProvider; + this.gitMonitor = gitMonitor; + this.logger = logger; + this.threadHandling = threadHandling; + + InitializationProcessor = initializationProcessorFactory.Create([gitMonitor], + _ => threadHandling.RunOnUIThreadAsync(() => + { + if (disposed) + { + return; + } + gitMonitor.HeadChanged += GitMonitor_OnHeadChanged; + activeConfigScopeTracker.CurrentConfigurationScopeChanged += ActiveConfigScopeTracker_OnCurrentConfigurationScopeChanged; + })); + } + + private void ActiveConfigScopeTracker_OnCurrentConfigurationScopeChanged(object sender, ConfigurationScopeChangedEventArgs e) + { + if (e.DefinitionChanged) + { + gitMonitor.Refresh(); + } + } + + private void GitMonitor_OnHeadChanged(object sender, EventArgs e) => + threadHandling.RunOnBackgroundThread(() => + { + if (!serviceProvider.TryGetTransientService(out ISonarProjectBranchSlCoreService sonarProjectBranchSlCoreService)) + { + logger.LogVerbose(SLCoreStrings.ServiceProviderNotInitialized); + return; + } + if (activeConfigScopeTracker.Current == null) + { + return; + } + sonarProjectBranchSlCoreService.DidVcsRepositoryChange(new DidVcsRepositoryChangeParams(activeConfigScopeTracker.Current.Id)); + }).Forget(); + + public void Dispose() + { + if (disposed) + { + return; + } + + if (InitializationProcessor.IsFinalized) + { + gitMonitor.HeadChanged -= GitMonitor_OnHeadChanged; + activeConfigScopeTracker.CurrentConfigurationScopeChanged -= ActiveConfigScopeTracker_OnCurrentConfigurationScopeChanged; + } + disposed = true; + } + + public IInitializationProcessor InitializationProcessor { get; } +} diff --git a/src/ConnectedMode/StatefulServerBranchProvider.cs b/src/ConnectedMode/StatefulServerBranchProvider.cs deleted file mode 100644 index 8dbf9aca17..0000000000 --- a/src/ConnectedMode/StatefulServerBranchProvider.cs +++ /dev/null @@ -1,161 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using Microsoft.VisualStudio.Threading; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.Core.ConfigurationScope; -using SonarLint.VisualStudio.Core.Synchronization; -using SonarLint.VisualStudio.SLCore; -using SonarLint.VisualStudio.SLCore.Core; -using SonarLint.VisualStudio.SLCore.Service.Branch; - -namespace SonarLint.VisualStudio.ConnectedMode -{ - [Export(typeof(IStatefulServerBranchProvider))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal sealed class StatefulServerBranchProvider : IStatefulServerBranchProvider, IDisposable - { - private readonly IServerBranchProvider serverBranchProvider; - private readonly IActiveSolutionBoundTracker activeSolutionBoundTracker; - private readonly IActiveConfigScopeTracker activeConfigScopeTracker; - private readonly ISLCoreServiceProvider serviceProvider; - private readonly ILogger logger; - private readonly IThreadHandling threadHandling; - private bool disposedValue; - private string selectedBranch; - private readonly IAsyncLock asyncLock; - - [ImportingConstructor] - public StatefulServerBranchProvider( - IServerBranchProvider serverBranchProvider, - IActiveSolutionBoundTracker activeSolutionBoundTracker, - IActiveConfigScopeTracker activeConfigScopeTracker, - ISLCoreServiceProvider serviceProvider, - ILogger logger, - IThreadHandling threadHandling, - IAsyncLockFactory asyncLockFactory) - { - this.serverBranchProvider = serverBranchProvider; - this.activeSolutionBoundTracker = activeSolutionBoundTracker; - this.activeConfigScopeTracker = activeConfigScopeTracker; - this.serviceProvider = serviceProvider; - this.logger = logger; - this.threadHandling = threadHandling; - asyncLock = asyncLockFactory.Create(); - - activeSolutionBoundTracker.PreSolutionBindingUpdated += OnPreSolutionBindingUpdated; - activeSolutionBoundTracker.PreSolutionBindingChanged += OnPreSolutionBindingChanged; - } - - private void OnPreSolutionBindingUpdated(object sender, EventArgs e) - { - logger.LogVerbose(Resources.StatefulBranchProvider_BindingUpdated); - SafeClearSelectedBranchCache(); - - NotifySlCoreBranchChange(); - } - - private void SafeClearSelectedBranchCache() - { - using (asyncLock.Acquire()) - { - selectedBranch = null; - } - } - - private void OnPreSolutionBindingChanged(object sender, ActiveSolutionBindingEventArgs e) - { - logger.LogVerbose(Resources.StatefulBranchProvider_BindingChanged); - SafeClearSelectedBranchCache(); - - if (e.Configuration.Mode.IsInAConnectedMode()) - { - NotifySlCoreBranchChange(); - } - } - - private void NotifySlCoreBranchChange() => - threadHandling.RunOnBackgroundThread(() => - { - if (!serviceProvider.TryGetTransientService(out ISonarProjectBranchSlCoreService sonarProjectBranchSlCoreService)) - { - logger.LogVerbose(SLCoreStrings.ServiceProviderNotInitialized); - return; - } - if (activeConfigScopeTracker.Current == null) - { - return; - } - sonarProjectBranchSlCoreService.DidVcsRepositoryChange(new DidVcsRepositoryChangeParams(activeConfigScopeTracker.Current.Id)); - }).Forget(); - - public async Task GetServerBranchNameAsync(CancellationToken token) - { - using (await asyncLock.AcquireAsync()) - { - if (selectedBranch == null) - { - logger.LogVerbose(Resources.StatefulBranchProvider_CacheMiss); - - // Note: we're using null to indicate that a refresh is required. - // However, the serverBranchProvider will return null in some cases e.g. standalone mode, not a git repo. - // In these cases we expect the serverBranchProvider to return quickly so the impact of making unnecessary - // calls is not significant. - selectedBranch = await DoGetServerBranchNameAsync(token); - } - else - { - logger.LogVerbose(Resources.StatefulBranchProvider_CacheHit); - } - } - - logger.WriteLine(Resources.StatefulBranchProvider_ReturnValue, selectedBranch ?? Resources.NullBranchName); - return selectedBranch; - } - - private Task DoGetServerBranchNameAsync(CancellationToken token) => threadHandling.RunOnBackgroundThread(() => serverBranchProvider.GetServerBranchNameAsync(token)); - - #region IDisposable - - private void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - activeSolutionBoundTracker.PreSolutionBindingChanged -= OnPreSolutionBindingChanged; - activeSolutionBoundTracker.PreSolutionBindingUpdated -= OnPreSolutionBindingUpdated; - } - disposedValue = true; - } - } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - #endregion IDisposable - } -} diff --git a/src/ConnectedMode/Suppressions/IRoslynSuppressionUpdater.cs b/src/ConnectedMode/Suppressions/IRoslynSuppressionUpdater.cs deleted file mode 100644 index 37e5c99f75..0000000000 --- a/src/ConnectedMode/Suppressions/IRoslynSuppressionUpdater.cs +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode.Suppressions; - -/// -/// Fetches suppressed issues from the server and raises events. This is mainly needed for Roslyn languages -/// -public interface IRoslynSuppressionUpdater -{ - /// - /// Fetches all available suppressions from the server and raises the event. - /// - Task UpdateAllServerSuppressionsAsync(); - - /// - /// For resolved issues, the event is invoked. - /// For unresolved issues (i.e. issues that are re-opened), the event is invoked. - /// - Task UpdateSuppressedIssuesAsync(bool isResolved, string[] issueKeys, CancellationToken cancellationToken); - - event EventHandler SuppressedIssuesReloaded; - event EventHandler NewIssuesSuppressed; - event EventHandler SuppressionsRemoved; -} - -public class SuppressionsEventArgs(IReadOnlyList suppressedIssues) : EventArgs -{ - public IReadOnlyList SuppressedIssues { get; } = suppressedIssues; -} - -public class SuppressionsRemovedEventArgs(IReadOnlyList issueServerKeys) : EventArgs -{ - public IReadOnlyList IssueServerKeys { get; } = issueServerKeys; -} diff --git a/src/ConnectedMode/Suppressions/RoslynSuppressionUpdater.cs b/src/ConnectedMode/Suppressions/RoslynSuppressionUpdater.cs deleted file mode 100644 index e1fab5e7dc..0000000000 --- a/src/ConnectedMode/Suppressions/RoslynSuppressionUpdater.cs +++ /dev/null @@ -1,144 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using SonarLint.VisualStudio.ConnectedMode.Helpers; -using SonarLint.VisualStudio.Core; -using SonarQube.Client; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode.Suppressions; - -[Export(typeof(IRoslynSuppressionUpdater))] -[PartCreationPolicy(CreationPolicy.Shared)] -internal sealed class RoslynSuppressionUpdater : IRoslynSuppressionUpdater, IDisposable -{ - private readonly ICancellableActionRunner actionRunner; - private readonly ILogger logger; - private readonly ISonarQubeService server; - private readonly IServerQueryInfoProvider serverQueryInfoProvider; - private readonly IThreadHandling threadHandling; - - [ImportingConstructor] - public RoslynSuppressionUpdater( - ISonarQubeService server, - IServerQueryInfoProvider serverQueryInfoProvider, - ICancellableActionRunner actionRunner, - ILogger logger, - IThreadHandling threadHandling) - { - this.server = server; - this.serverQueryInfoProvider = serverQueryInfoProvider; - this.actionRunner = actionRunner; - this.logger = logger.ForContext(Resources.ConnectedModeLogContext, Resources.RoslynSuppressionsLogContext); - this.threadHandling = threadHandling; - } - - public async Task UpdateAllServerSuppressionsAsync() - { - var suppressedIssues = await GetSuppressedIssuesAsync(); - if (suppressedIssues?.Count > 0) - { - InvokeSuppressedIssuesReloaded(suppressedIssues); - } - } - - public async Task UpdateSuppressedIssuesAsync(bool isResolved, string[] issueKeys, CancellationToken cancellationToken) - { - if (!issueKeys.Any()) - { - return; - } - if (!isResolved) - { - InvokeSuppressionsRemoved(issueKeys); - return; - } - - var suppressedIssues = await GetSuppressedIssuesAsync(issueKeys, cancellationToken); - if (suppressedIssues?.Count > 0) - { - InvokeNewIssuesSuppressed(suppressedIssues); - } - } - - public event EventHandler SuppressedIssuesReloaded; - public event EventHandler NewIssuesSuppressed; - public event EventHandler SuppressionsRemoved; - - public void Dispose() => actionRunner.Dispose(); - - private async Task> GetSuppressedIssuesAsync(string[] issueKeys = null, CancellationToken? cancellationToken = null) => - await threadHandling.RunOnBackgroundThread(async () => - { - IList suppressedIssues = null; - await actionRunner.RunAsync(async token => - { - suppressedIssues = await FetchSuppressedSonarQubeIssuesAsync(issueKeys, cancellationToken, token); - }); - - return suppressedIssues; - }); - - private async Task> FetchSuppressedSonarQubeIssuesAsync( - string[] issueKeys, - CancellationToken? cancellationToken, - CancellationToken token) - { - IList suppressedIssues = null; - try - { - var allServerIssuesFetched = issueKeys == null; - logger.WriteLine(Resources.Suppressions_Fetch_Issues, allServerIssuesFetched); - - var (projectKey, serverBranch) = await serverQueryInfoProvider.GetProjectKeyAndBranchAsync(token); - if (projectKey == null || serverBranch == null) - { - return null; - } - - ThrowIfCancelled(cancellationToken, token); - suppressedIssues = await server.GetSuppressedRoslynIssuesAsync(projectKey, serverBranch, issueKeys, token); - logger.WriteLine(Resources.Suppression_Fetch_Issues_Finished, allServerIssuesFetched); - } - catch (OperationCanceledException) - { - logger.WriteLine(Resources.Suppressions_FetchOperationCancelled); - } - catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) - { - logger.LogVerbose(Resources.Suppression_FetchError_Verbose, ex); - logger.WriteLine(Resources.Suppressions_FetchError_Short, ex.Message); - } - return suppressedIssues; - } - - private static void ThrowIfCancelled(CancellationToken? cancellationToken, CancellationToken token) - { - token.ThrowIfCancellationRequested(); - cancellationToken?.ThrowIfCancellationRequested(); - } - - private void InvokeSuppressedIssuesReloaded(IList allSuppressedIssues) => SuppressedIssuesReloaded?.Invoke(this, new SuppressionsEventArgs(allSuppressedIssues.ToList())); - - private void InvokeSuppressionsRemoved(IList suppressedIssueKeys) => SuppressionsRemoved?.Invoke(this, new SuppressionsRemovedEventArgs(suppressedIssueKeys.ToList())); - - private void InvokeNewIssuesSuppressed(IList newSuppressedIssues) => NewIssuesSuppressed?.Invoke(this, new SuppressionsEventArgs(newSuppressedIssues.ToList())); -} diff --git a/src/ConnectedMode/Suppressions/TimedUpdateHandler.cs b/src/ConnectedMode/Suppressions/TimedUpdateHandler.cs deleted file mode 100644 index 43ccaf93db..0000000000 --- a/src/ConnectedMode/Suppressions/TimedUpdateHandler.cs +++ /dev/null @@ -1,112 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using Microsoft.VisualStudio.Threading; -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.Core.SystemAbstractions; - -namespace SonarLint.VisualStudio.ConnectedMode.Suppressions -{ - [Export(typeof(TimedUpdateHandler))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal sealed class TimedUpdateHandler : IDisposable - { - private const double MillisecondsToWaitBetweenRefresh = 1000 * 60 * 10; // 10 minutes - - private readonly ITimer refreshTimer; - private readonly IRoslynSuppressionUpdater roslynSuppressionUpdater; - private readonly IQualityProfileUpdater qualityProfileUpdater; - private readonly ILogger logger; - private readonly IActiveSolutionBoundTracker activeSolutionBoundTracker; - - private bool disposed; - - [ImportingConstructor] - public TimedUpdateHandler( - IRoslynSuppressionUpdater roslynSuppressionUpdater, - IQualityProfileUpdater qualityProfileUpdater, - ILogger logger, - IActiveSolutionBoundTracker activeSolutionBoundTracker) - : this(roslynSuppressionUpdater, qualityProfileUpdater, activeSolutionBoundTracker, logger, new TimerFactory()) - { - } - - internal /* for testing */ TimedUpdateHandler( - IRoslynSuppressionUpdater roslynSuppressionUpdater, - IQualityProfileUpdater qualityProfileUpdater, - IActiveSolutionBoundTracker activeSolutionBoundTracker, - ILogger logger, - ITimerFactory timerFactory) - { - this.roslynSuppressionUpdater = roslynSuppressionUpdater; - this.qualityProfileUpdater = qualityProfileUpdater; - this.logger = logger; - this.activeSolutionBoundTracker = activeSolutionBoundTracker; - - refreshTimer = timerFactory.Create(); - refreshTimer.AutoReset = true; - refreshTimer.Interval = MillisecondsToWaitBetweenRefresh; - refreshTimer.Elapsed += OnRefreshTimerElapsed; - - activeSolutionBoundTracker.SolutionBindingChanged += SolutionBindingChanged; - - SetTimerStatus(activeSolutionBoundTracker.CurrentConfiguration); - } - - private void SolutionBindingChanged(object sender, ActiveSolutionBindingEventArgs activeSolutionBindingEventArgs) - { - SetTimerStatus(activeSolutionBindingEventArgs.Configuration); - } - - private void SetTimerStatus(BindingConfiguration bindingConfiguration) - { - if (bindingConfiguration?.Mode != SonarLintMode.Standalone) - { - refreshTimer.Start(); - } - else - { - refreshTimer.Stop(); - } - } - - private void OnRefreshTimerElapsed(object sender, TimerEventArgs e) - { - logger.WriteLine(Resources.TimedUpdateTriggered); - roslynSuppressionUpdater.UpdateAllServerSuppressionsAsync().Forget(); - qualityProfileUpdater.UpdateAsync().Forget(); - } - - public void Dispose() - { - if (!disposed) - { - refreshTimer.Elapsed -= OnRefreshTimerElapsed; - refreshTimer.Dispose(); - disposed = true; - - activeSolutionBoundTracker.SolutionBindingChanged -= SolutionBindingChanged; - } - } - } -} diff --git a/src/ConnectedMode/Transition/MuteIssuesService.cs b/src/ConnectedMode/Transition/MuteIssuesService.cs index f8dc6d6ccf..f21ac66279 100644 --- a/src/ConnectedMode/Transition/MuteIssuesService.cs +++ b/src/ConnectedMode/Transition/MuteIssuesService.cs @@ -19,7 +19,6 @@ */ using System.ComponentModel.Composition; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.ConfigurationScope; using SonarLint.VisualStudio.Core.Suppressions; @@ -39,8 +38,6 @@ internal class MuteIssuesService( IMuteIssuesWindowService muteIssuesWindowService, IActiveConfigScopeTracker activeConfigScopeTracker, ISLCoreServiceProvider slCoreServiceProvider, - IServerIssueFinder serverIssueFinder, - IRoslynSuppressionUpdater roslynSuppressionUpdater, ILogger logger, IThreadHandling threadHandling) : IMuteIssuesService @@ -50,34 +47,20 @@ internal class MuteIssuesService( public async Task ResolveIssueWithDialogAsync(IFilterableIssue issue) { threadHandling.ThrowIfOnUIThread(); - var issueServerKey = await GetIssueServerKeyAsync(issue); + var issueServerKey = GetIssueServerKey(issue); var currentConfigScope = activeConfigScopeTracker.Current; CheckIsInConnectedMode(currentConfigScope); CheckIssueServerKeyNotNullOrEmpty(issueServerKey); var allowedStatuses = await GetAllowedStatusesAsync(currentConfigScope.ConnectionId, issueServerKey); var windowResponse = await PromptMuteIssueResolutionAsync(allowedStatuses); - await MuteIssueAsync(currentConfigScope.Id, issueServerKey, issue, windowResponse.IssueTransition.Value); + await MuteIssueAsync(currentConfigScope.Id, issueServerKey, windowResponse.IssueTransition.Value); await AddCommentAsync(currentConfigScope.Id, issueServerKey, windowResponse.Comment); } - private async Task GetIssueServerKeyAsync(IFilterableIssue issue) - { - // Non-Roslyn issues already have the issue server key - if (issue is IAnalysisIssueVisualization issueViz) - { - return issueViz.Issue.IssueServerKey; - } - - // Roslyn issues need to be converted to SonarQube issues to get the server key as they are handled by SLCore - var serverIssue = await serverIssueFinder.FindServerIssueAsync(issue, CancellationToken.None); - if (serverIssue is { IsResolved: true }) - { - logger.WriteLine(Resources.MuteIssue_ErrorIssueAlreadyResolved); - throw new MuteIssueException(Resources.MuteIssue_ErrorIssueAlreadyResolved); - } - return serverIssue?.IssueKey; - } + private static string GetIssueServerKey(IFilterableIssue issue) => + // TODO by https://sonarsource.atlassian.net/browse/SLVS-2419 remove handling of different type of issues + ((IAnalysisIssueVisualization)issue).Issue.IssueServerKey; private async Task PromptMuteIssueResolutionAsync(IEnumerable allowedStatuses) { @@ -153,7 +136,6 @@ private async Task> GetAllowedStatusesAsync(string connec private async Task MuteIssueAsync( string configurationScopeId, string issueServerKey, - IFilterableIssue issue, SonarQubeIssueTransition transition) { try @@ -166,7 +148,6 @@ await issueSlCoreService.ChangeStatusAsync(new ChangeIssueStatusParams transition.ToSlCoreResolutionStatus(), false // Muting taints are not supported yet )); - await UpdateRoslynSuppressionsAsync(issue, issueServerKey); } catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) { @@ -191,15 +172,4 @@ private async Task AddCommentAsync(string configurationScopeId, string issueServ throw new MuteIssueException.MuteIssueCommentFailedException(); } } - - /// - /// The suppressed issues for roslyn are not dealt by SlCore, but are stored on disk, so we need to update them manually - /// - private async Task UpdateRoslynSuppressionsAsync(IFilterableIssue issue, string serverIssueKey) - { - if (issue is IFilterableRoslynIssue) - { - await roslynSuppressionUpdater.UpdateSuppressedIssuesAsync(isResolved: true, [serverIssueKey], new CancellationToken()); - } - } } diff --git a/src/ConnectedMode/packages.lock.json b/src/ConnectedMode/packages.lock.json index 30aae1ca8d..f81f4a1b36 100644 --- a/src/ConnectedMode/packages.lock.json +++ b/src/ConnectedMode/packages.lock.json @@ -163,16 +163,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "Humanizer.Core": { "type": "Transitive", "resolved": "2.2.0", @@ -1398,8 +1388,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/Core.UnitTests/Analysis/RoslynQuickFixTests.cs b/src/Core.UnitTests/Analysis/RoslynQuickFixTests.cs new file mode 100644 index 0000000000..300dfc3da3 --- /dev/null +++ b/src/Core.UnitTests/Analysis/RoslynQuickFixTests.cs @@ -0,0 +1,84 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Core.Analysis; + +namespace SonarLint.VisualStudio.Core.UnitTests.Analysis; + +[TestClass] +public class RoslynQuickFixTests +{ + public static object[][] IdAndStorageValueTestData => + [ + [new Guid("64146AA8-AE93-4ABE-BD81-FCBE27FC4D3E"), "||64146aa8-ae93-4abe-bd81-fcbe27fc4d3e"], + [new Guid("6125AA53-4A10-4A07-84CD-9C4C758B26A3"), "||6125aa53-4a10-4a07-84cd-9c4c758b26a3"], + [new Guid("145A5840-0E94-41D1-8431-4BFA07F952C6"), "||145a5840-0e94-41d1-8431-4bfa07f952c6"] + ]; + + [TestMethod] + [DynamicData(nameof(IdAndStorageValueTestData))] + public void StorageValue_ContainsCorrectPrefix(Guid id, string expectedStorageValue) + { + var testSubject = new RoslynQuickFix(id); + + testSubject.Id.Should().Be(id); + testSubject.GetStorageValue().Should().Be(expectedStorageValue); + } + + [TestMethod] + [DynamicData(nameof(IdAndStorageValueTestData))] + public void TryParse_ValidInput_ReturnsTrue(Guid expectedId, string storageValue) + { + var result = RoslynQuickFix.TryParse(storageValue, out var quickFix); + + result.Should().BeTrue(); + quickFix.Should().NotBeNull(); + quickFix.Id.Should().Be(expectedId); + } + + [TestMethod] + [DataRow("")] + [DataRow(null)] + [DataRow("invalid")] + [DataRow("||")] + [DataRow("||not-a-guid")] + [DataRow("||BCF7C738-EEF5-4F41-A5FF-3D8DDC00540B")] + [DataRow("82D00A1A-3019-4E09-9895-BE42657DFB34")] + public void TryParse_InvalidInput_ReturnsFalse(string message) + { + var result = RoslynQuickFix.TryParse(message, out _); + + result.Should().BeFalse(); + } + + [TestMethod] + public void TryParse_RoundTrip_PreservesOriginalId() + { + var originalId = Guid.NewGuid(); + var originalQuickFix = new RoslynQuickFix(originalId); + var storageValue = originalQuickFix.GetStorageValue(); + + var success = RoslynQuickFix.TryParse(storageValue, out var parsedQuickFix); + + success.Should().BeTrue(); + parsedQuickFix.Id.Should().Be(originalId); + parsedQuickFix.GetStorageValue().Should().Be(storageValue); + } +} diff --git a/src/Core.UnitTests/Binding/BoundServerProjectTests.cs b/src/Core.UnitTests/Binding/BoundServerProjectTests.cs index 84ace35b94..2f8ccf322e 100644 --- a/src/Core.UnitTests/Binding/BoundServerProjectTests.cs +++ b/src/Core.UnitTests/Binding/BoundServerProjectTests.cs @@ -66,6 +66,5 @@ public void Ctor_SetsValues() boundServerProject.LocalBindingKey.Should().BeSameAs(localBindingKey); boundServerProject.ServerProjectKey.Should().BeSameAs(serverProjectKey); boundServerProject.ServerConnection.Should().BeSameAs(serverConnection); - boundServerProject.Profiles.Should().BeNull(); } } diff --git a/src/Core.UnitTests/LanguageTests.cs b/src/Core.UnitTests/LanguageTests.cs index cd9c82eef2..049e1c6e30 100644 --- a/src/Core.UnitTests/LanguageTests.cs +++ b/src/Core.UnitTests/LanguageTests.cs @@ -32,27 +32,29 @@ public void Language_Ctor_ArgChecks() // Arrange var key = "k"; var name = "MyName"; - var fileSuffix = "suffix"; var serverLanguageKey = "serverLanguageKey"; // Act + Assert // Nulls - Action act = () => new Language(name, null, serverLanguageKey, pluginInfo, repoInfo, settingsFileName: fileSuffix); + Action act = () => new Language(name, null, serverLanguageKey, pluginInfo, repoInfo); act.Should().ThrowExactly().And.ParamName.Should().Be("name"); - act = () => new Language(null, key, serverLanguageKey, pluginInfo, repoInfo, settingsFileName: fileSuffix); + act = () => new Language(null, key, serverLanguageKey, pluginInfo, repoInfo); act.Should().ThrowExactly().And.ParamName.Should().Be("id"); - act = () => new Language(name, key, null, pluginInfo, repoInfo, settingsFileName: fileSuffix); + act = () => new Language(name, key, null, pluginInfo, repoInfo); act.Should().ThrowExactly().And.ParamName.Should().Be("serverLanguageKey"); act = () => new Language(name, key, serverLanguageKey, null, repoInfo, repoInfo); act.Should().ThrowExactly().And.ParamName.Should().Be("pluginInfo"); - act = () => new Language(name, key, serverLanguageKey, pluginInfo, null, settingsFileName: fileSuffix); + act = () => new Language(name, key, serverLanguageKey, pluginInfo, null); act.Should().ThrowExactly().And.ParamName.Should().Be("repoInfo"); - act = () => new Language(name, key, serverLanguageKey, pluginInfo, repoInfo, securityRepoInfo: null, settingsFileName: fileSuffix); + act = () => new Language(name, key, serverLanguageKey, pluginInfo, repoInfo, securityRepoInfo: null); + act.Should().NotThrow(); + + act = () => new Language(name, key, serverLanguageKey, pluginInfo, repoInfo, additionalPlugins: null); act.Should().NotThrow(); } @@ -133,8 +135,10 @@ public void HasRepoKey_RepoNorSecurityRepoHasKey_ReturnsFalse() [TestMethod] public void Language_HasCorrectPlugin() { - LanguageHasExpectedPlugin(Language.CSharp, "csharpenterprise", "sonar-csharp-enterprise-plugin-(\\d+\\.\\d+\\.\\d+\\.\\d+)\\.jar"); - LanguageHasExpectedPlugin(Language.VBNET, "vbnetenterprise", "sonar-vbnet-enterprise-plugin-(\\d+\\.\\d+\\.\\d+\\.\\d+)\\.jar"); + LanguageHasExpectedPlugin(Language.CSharp, "sqvsroslyn", "sonarqube-ide-visualstudio-roslyn-plugin-(\\d+\\.\\d+\\.\\d+\\.\\d+)\\.jar"); + LanguageHasExpectedAdditionalPlugins(Language.CSharp, [new("csharpenterprise", "sonar-csharp-enterprise-plugin-(\\d+\\.\\d+\\.\\d+\\.\\d+)\\.jar", isEnabledForAnalysis: false)]); + LanguageHasExpectedPlugin(Language.VBNET, "sqvsroslyn", "sonarqube-ide-visualstudio-roslyn-plugin-(\\d+\\.\\d+\\.\\d+\\.\\d+)\\.jar"); + LanguageHasExpectedAdditionalPlugins(Language.VBNET, [new("vbnetenterprise", "sonar-vbnet-enterprise-plugin-(\\d+\\.\\d+\\.\\d+\\.\\d+)\\.jar", isEnabledForAnalysis: false)]); LanguageHasExpectedPlugin(Language.Cpp, "cpp", "sonar-cfamily-plugin-(\\d+\\.\\d+\\.\\d+\\.\\d+)\\.jar"); LanguageHasExpectedPlugin(Language.C, "cpp", "sonar-cfamily-plugin-(\\d+\\.\\d+\\.\\d+\\.\\d+)\\.jar"); @@ -171,6 +175,17 @@ private static void LanguageHasExpectedPlugin(Language language, string pluginKe { language.PluginInfo.Key.Should().Be(pluginKey); language.PluginInfo.FilePattern.Should().Be(filePattern); + language.PluginInfo.IsEnabledForAnalysis.Should().Be(true); + } + + private static void LanguageHasExpectedAdditionalPlugins(Language language, List expectedPlugins) + { + foreach (var expectedPlugin in expectedPlugins) + { + language.AdditionalPlugins.Should().Contain(x => x.Key == expectedPlugin.Key && + x.FilePattern == expectedPlugin.FilePattern && + x.IsEnabledForAnalysis == expectedPlugin.IsEnabledForAnalysis); + } } private static void LanguageHasExpectedRepoInfo(Language language, string repoKey, string folderName) => HasExpectedRepoInfo(language.RepoInfo, repoKey, folderName); diff --git a/src/Core.UnitTests/PluginInfoTests.cs b/src/Core.UnitTests/PluginInfoTests.cs index 3a221ee95d..1a1b092f80 100644 --- a/src/Core.UnitTests/PluginInfoTests.cs +++ b/src/Core.UnitTests/PluginInfoTests.cs @@ -16,4 +16,24 @@ public void PluginInfo_NullFilePattern_DoesNotThrow() Action act = () => new PluginInfo("csharp", null); act.Should().NotThrow(); } + + [TestMethod] + [DataRow("csharp", "csharp-plugin-(\\d+\\).jar", true)] + [DataRow("vbnet", "vbnet-plugin-(\\d+\\).jar", false)] + public void PluginInfo_InitializesParameters(string key, string pattern, bool isEnabled) + { + var pluginInfo = new PluginInfo(key, pattern, isEnabled); + + pluginInfo.Key.Should().Be(key); + pluginInfo.FilePattern.Should().Be(pattern); + pluginInfo.IsEnabledForAnalysis.Should().Be(isEnabled); + } + + [TestMethod] + public void PluginInfo_IsEnabledDefaultsToTrue() + { + var pluginInfo = new PluginInfo("csharp", "csharp-plugin-(\\d+\\).jar"); + + pluginInfo.IsEnabledForAnalysis.Should().Be(true); + } } diff --git a/src/Core.UnitTests/ServerSentEvents/ServerEventChannelTests.cs b/src/Core.UnitTests/ServerSentEvents/ServerEventChannelTests.cs deleted file mode 100644 index 6a555982de..0000000000 --- a/src/Core.UnitTests/ServerSentEvents/ServerEventChannelTests.cs +++ /dev/null @@ -1,159 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using SonarLint.VisualStudio.Core.ServerSentEvents; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarLint.VisualStudio.Core.UnitTests.ServerSentEvents -{ - [TestClass] - public class ServerEventChannelTests - { - [TestMethod] - public async Task Get_AfterPublish_ReturnsCorrectValue() - { - var testSubject = CreateTestSubject(); - var serverEvent = Mock.Of(); - - testSubject.Publish(serverEvent); - var receivedEvent = await testSubject.GetNextEventOrNullAsync(); - - receivedEvent.Should().BeSameAs(serverEvent); - } - - [TestMethod] - public async Task Get_AwaitsForPublishBeforeReturning() - { - var testSubject = CreateTestSubject(); - var serverEvent = Mock.Of(); - - var getTask = testSubject.GetNextEventOrNullAsync(); - testSubject.Publish(serverEvent); - var receivedEvent = await getTask; - - receivedEvent.Should().BeSameAs(serverEvent); - } - - [TestMethod] - public async Task Get_ReturnsMultiplePublishedItemsInCorrectOrder() - { - var testSubject = CreateTestSubject(); - var serverEvents = Enumerable.Range(0, 5).Select(_ => Mock.Of()).ToList(); - var receivedEvents = new List(); - - foreach (var serverEvent in serverEvents) - { - testSubject.Publish(serverEvent); - } - for (var i = 0; i < serverEvents.Count; i++) - { - receivedEvents.Add(await testSubject.GetNextEventOrNullAsync()); - } - - receivedEvents.Should().BeEquivalentTo(serverEvents); - } - - [TestMethod] - [DataRow(true)] - [DataRow(false)] - public async Task Get_AlreadyDisposedAndNoMoreItems_ReturnsNull(bool isEmptyChannel) - { - var testSubject = CreateTestSubject(); - var serverEvent = Mock.Of(); - - if (!isEmptyChannel) - { - testSubject.Publish(serverEvent); - } - testSubject.Dispose(); - var receivedEvent = await testSubject.GetNextEventOrNullAsync(); - var receivedEvent2 = await testSubject.GetNextEventOrNullAsync(); - - receivedEvent.Should().BeSameAs(isEmptyChannel ? null : serverEvent); - receivedEvent2.Should().BeNull(); - } - - [TestMethod] - public async Task Get_AfterDispose_ReturnsNull() - { - var testSubject = CreateTestSubject(); - - testSubject.Dispose(); - var receivedEvent = await testSubject.GetNextEventOrNullAsync(); - - receivedEvent.Should().BeNull(); - } - - [TestMethod] - public async Task Get_DisposedWhileAwaiting_ReturnsNull() - { - var testSubject = CreateTestSubject(); - - var getTask = testSubject.GetNextEventOrNullAsync(); - testSubject.Dispose(); - var receivedEvent = await getTask; - - receivedEvent.Should().BeNull(); - } - - [TestMethod] - public void Publish_AlreadyDisposed_ThrowsObjectDisposedException() - { - var testSubject = CreateTestSubject(); - var serverEvent = Mock.Of(); - - testSubject.Publish(serverEvent); - testSubject.Dispose(); - Assert.ThrowsException(() => testSubject.Publish(serverEvent)); - } - - [TestMethod] - public void Publish_WhenEventIsNull_ThrowsArgumentNullException() - { - var testSubject = CreateTestSubject(); - - Assert.ThrowsException(() => testSubject.Publish(null)); - } - - [TestMethod] - public void Dispose_IsIdempotent() - { - var testSubject = CreateTestSubject(); - var serverEvent = Mock.Of(); - - testSubject.Dispose(); - testSubject.Dispose(); - - Assert.ThrowsException(() => testSubject.Publish(serverEvent)); - } - - private ServerEventChannel CreateTestSubject() - { - return new ServerEventChannel(); - } - } -} diff --git a/src/Core.UnitTests/SonarCompositeRuleIdTests.cs b/src/Core.UnitTests/SonarCompositeRuleIdTests.cs index cfd6fc8d2a..20ad1629dc 100644 --- a/src/Core.UnitTests/SonarCompositeRuleIdTests.cs +++ b/src/Core.UnitTests/SonarCompositeRuleIdTests.cs @@ -18,10 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; - namespace SonarLint.VisualStudio.Core.UnitTests { [TestClass] @@ -85,7 +81,20 @@ public void ToString_ReturnsExpected() output.ToString().Should().Be(input); } + [TestMethod] + [DataRow("repo1", "rule1", "repo1:rule1")] + [DataRow("csharpsquid", "S1234", "csharpsquid:S1234")] + [DataRow("javascript", "S5678", "javascript:S5678")] + [DataRow("my repo", "my rule", "my repo:my rule")] + public void GetFullErrorCode_ConcatenatesWithColon(string repoKey, string ruleKey, string expected) + { + var result = SonarCompositeRuleId.GetFullErrorCode(repoKey, ruleKey); + + result.Should().Be(expected); + } + public static object[][] ReposAndLanguages => LanguageProvider.Instance.AllKnownLanguages.Select(x => new object[] { x.RepoInfo.Key, x }).ToArray(); + [DataTestMethod] [DynamicData(nameof(ReposAndLanguages))] public void Language_MatchesBasedOnRepoKey(string repoKey, Language language) diff --git a/src/Core.UnitTests/packages.lock.json b/src/Core.UnitTests/packages.lock.json index 33a58cfe9b..3e3440eb3a 100644 --- a/src/Core.UnitTests/packages.lock.json +++ b/src/Core.UnitTests/packages.lock.json @@ -111,16 +111,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -1308,8 +1298,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/Core/Analysis/AnalysisIssue.cs b/src/Core/Analysis/AnalysisIssue.cs index 9f4a9718a0..5f885c058b 100644 --- a/src/Core/Analysis/AnalysisIssue.cs +++ b/src/Core/Analysis/AnalysisIssue.cs @@ -23,7 +23,7 @@ namespace SonarLint.VisualStudio.Core.Analysis public class AnalysisIssue : IAnalysisIssue { private static readonly IReadOnlyList EmptyFlows = []; - private static readonly IReadOnlyList EmptyFixes = []; + private static readonly IReadOnlyList EmptyFixes = []; public AnalysisIssue( Guid? id, @@ -35,7 +35,7 @@ public AnalysisIssue( Impact highestImpact, IAnalysisIssueLocation primaryLocation, IReadOnlyList flows, - IReadOnlyList fixes = null) + IReadOnlyList fixes = null) { Id = id; RuleKey = ruleKey; @@ -63,7 +63,7 @@ public AnalysisIssue( public bool IsResolved { get; } public string IssueServerKey { get; } - public IReadOnlyList Fixes { get; } + public IReadOnlyList Fixes { get; } public Impact HighestImpact { get; } } @@ -80,7 +80,7 @@ public AnalysisHotspotIssue( IAnalysisIssueLocation primaryLocation, IReadOnlyList flows, HotspotStatus hotspotStatus, - IReadOnlyList fixes = null, + IReadOnlyList fixes = null, HotspotPriority? hotspotPriority = null) : base(id, ruleKey, issueServerKey, isResolved, severity, type, highestImpact, primaryLocation, flows, fixes) { diff --git a/src/Core/Analysis/IAnalysisIssue.cs b/src/Core/Analysis/IAnalysisIssue.cs index 966c6f44c5..61879585b3 100644 --- a/src/Core/Analysis/IAnalysisIssue.cs +++ b/src/Core/Analysis/IAnalysisIssue.cs @@ -26,7 +26,7 @@ public interface IAnalysisIssue : IAnalysisIssueBase AnalysisIssueType? Type { get; } - IReadOnlyList Fixes { get; } + IReadOnlyList Fixes { get; } Impact HighestImpact { get; } } diff --git a/src/Core/Analysis/IQuickFix.cs b/src/Core/Analysis/IQuickFix.cs index 4f587ed089..3d7110e0a0 100644 --- a/src/Core/Analysis/IQuickFix.cs +++ b/src/Core/Analysis/IQuickFix.cs @@ -18,11 +18,16 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Collections.Generic; - namespace SonarLint.VisualStudio.Core.Analysis { - public interface IQuickFix + public interface IQuickFixBase; + + public interface IRoslynQuickFix : IQuickFixBase + { + Guid Id { get; } + } + + public interface ITextBasedQuickFix : IQuickFixBase { string Message { get; } IReadOnlyList Edits { get; } diff --git a/src/SonarQube.Client/Models/SonarQubeProject.cs b/src/Core/Analysis/RoslynQuickFix.cs similarity index 60% rename from src/SonarQube.Client/Models/SonarQubeProject.cs rename to src/Core/Analysis/RoslynQuickFix.cs index db256beb44..f5763a6ce8 100644 --- a/src/SonarQube.Client/Models/SonarQubeProject.cs +++ b/src/Core/Analysis/RoslynQuickFix.cs @@ -18,22 +18,25 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; +namespace SonarLint.VisualStudio.Core.Analysis; -namespace SonarQube.Client.Models +public class RoslynQuickFix(Guid id) : IRoslynQuickFix { - public class SonarQubeProject - { - // Ordinal comparer should be good enough: http://docs.sonarqube.org/display/SONAR/Project+Administration#ProjectAdministration-AddingaProject - public static readonly StringComparer KeyComparer = StringComparer.Ordinal; + private const string StoragePrefix = "||"; + + public Guid Id { get; } = id; - public string Key { get; } - public string Name { get; } + public string GetStorageValue() => StoragePrefix + Id; - public SonarQubeProject(string key, string name) + public static bool TryParse(string message, out RoslynQuickFix o) + { + if (message is not null && message.StartsWith(StoragePrefix) && Guid.TryParse(message.Substring(StoragePrefix.Length), out var id)) { - Key = key; - Name = name; + o = new RoslynQuickFix(id); + return true; } + + o = null; + return false; } } diff --git a/src/Core/Analysis/QuickFix.cs b/src/Core/Analysis/TextBasedQuickFix.cs similarity index 92% rename from src/Core/Analysis/QuickFix.cs rename to src/Core/Analysis/TextBasedQuickFix.cs index 8fbdffe546..22a7267fe2 100644 --- a/src/Core/Analysis/QuickFix.cs +++ b/src/Core/Analysis/TextBasedQuickFix.cs @@ -23,9 +23,9 @@ namespace SonarLint.VisualStudio.Core.Analysis { - public class QuickFix : IQuickFix + public class TextBasedQuickFix : ITextBasedQuickFix { - public QuickFix(string message, IReadOnlyList edits) + public TextBasedQuickFix(string message, IReadOnlyList edits) { if (edits == null || edits.Count == 0) { diff --git a/src/Core/Binding/BoundServerProject.cs b/src/Core/Binding/BoundServerProject.cs index a5a6a53009..2969ce41af 100644 --- a/src/Core/Binding/BoundServerProject.cs +++ b/src/Core/Binding/BoundServerProject.cs @@ -25,7 +25,6 @@ public class BoundServerProject public string LocalBindingKey { get; } public string ServerProjectKey { get; } public ServerConnection ServerConnection { get; } - public Dictionary Profiles { get; set; } public BoundServerProject(string localBindingKey, string serverProjectKey, ServerConnection serverConnection) { diff --git a/src/Core/Binding/IActiveSolutionBoundTracker.cs b/src/Core/Binding/IActiveSolutionBoundTracker.cs index 9181025b18..3fc5a894ca 100644 --- a/src/Core/Binding/IActiveSolutionBoundTracker.cs +++ b/src/Core/Binding/IActiveSolutionBoundTracker.cs @@ -57,17 +57,6 @@ public interface IActiveSolutionBoundTracker : IRequireInitialization /// opening/closing. /// event EventHandler SolutionBindingChanged; - - /// - /// Raised when an existing binding has been updated, and before the event - /// - event EventHandler PreSolutionBindingUpdated; - - /// - /// Raised when an existing binding has been updated i.e. the solution is still bound to the same - /// Sonar project (e.g. the user updated the binding via the Team Explorer) - /// - event EventHandler SolutionBindingUpdated; } public class ActiveSolutionBindingEventArgs : EventArgs diff --git a/src/Core/CSharpVB/IRoslynConfigGenerator.cs b/src/Core/CSharpVB/IRoslynConfigGenerator.cs deleted file mode 100644 index c40066f926..0000000000 --- a/src/Core/CSharpVB/IRoslynConfigGenerator.cs +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -namespace SonarLint.VisualStudio.Core.CSharpVB; - -public interface IRoslynConfigGenerator -{ - void GenerateAndSaveConfiguration( - Language language, - string baseDirectory, - IDictionary properties, - IFileExclusions fileExclusions, - IReadOnlyCollection ruleStatuses, - IReadOnlyCollection ruleParameters); -} diff --git a/src/Core/CSharpVB/SonarLintConfiguration.cs b/src/Core/CSharpVB/SonarLintConfiguration.cs index 6fe138ff54..0aaa24d738 100644 --- a/src/Core/CSharpVB/SonarLintConfiguration.cs +++ b/src/Core/CSharpVB/SonarLintConfiguration.cs @@ -63,7 +63,7 @@ public class SonarLintConfiguration [XmlArrayItem(ElementName = "Setting")] public List Settings { get; set; } = new List(); - [ XmlArrayItem(ElementName = "Rule")] + [XmlArrayItem(ElementName = "Rule")] public List Rules { get; set; } = new List(); } @@ -73,7 +73,7 @@ public class SonarLintRule public string Key { get; set; } [XmlArrayItem(ElementName = "Parameter")] - public List Parameters { get; set; } = new List(); + public List Parameters { get; set; } = []; } // The SonarLint.xml file has Settings elements and diff --git a/src/Core/ConfigurationScope/IActiveConfigScopeTracker.cs b/src/Core/ConfigurationScope/IActiveConfigScopeTracker.cs index 2893ecb465..da8576715b 100644 --- a/src/Core/ConfigurationScope/IActiveConfigScopeTracker.cs +++ b/src/Core/ConfigurationScope/IActiveConfigScopeTracker.cs @@ -34,6 +34,8 @@ public interface IActiveConfigScopeTracker : IDisposable bool TryUpdateAnalysisReadinessOnCurrentConfigScope(string id, bool isReady); + bool TryUpdateMatchedBranchOnCurrentConfigScope(string id, string branch); + event EventHandler CurrentConfigurationScopeChanged; } @@ -51,7 +53,8 @@ public record ConfigurationScope( string SonarProjectId = null, string RootPath = null, string CommandsBaseDir = null, - bool IsReadyForAnalysis = false) + bool IsReadyForAnalysis = false, + string MatchedBranch = null) { public string Id { get; } = Id ?? throw new ArgumentNullException(nameof(Id)); } diff --git a/src/Core/DocumentEvents.cs b/src/Core/DocumentEvents.cs index 5f89165288..834a12fa3b 100644 --- a/src/Core/DocumentEvents.cs +++ b/src/Core/DocumentEvents.cs @@ -22,18 +22,9 @@ namespace SonarLint.VisualStudio.Core; -public class DocumentEventArgs(Document document) : EventArgs +public class DocumentEventArgs(Document document, string content = null) : EventArgs { public Document Document { get; } = document; -} - -public class DocumentSavedEventArgs(Document document, string newContent) : DocumentEventArgs(document) -{ - public string NewContent { get; } = newContent; -} - -public class DocumentOpenedEventArgs(Document document, string content) : DocumentEventArgs(document) -{ public string Content { get; } = content; } @@ -54,8 +45,9 @@ public class Document(string fullPath, IEnumerable detectedLan public interface IDocumentTracker { event EventHandler DocumentClosed; - event EventHandler DocumentOpened; - event EventHandler DocumentSaved; + event EventHandler DocumentOpened; + event EventHandler DocumentSaved; + event EventHandler DocumentUpdated; /// /// Raised when an opened document is renamed /// diff --git a/src/SLCore/Common/Models/FileUri.cs b/src/Core/Helpers/FileUri.cs similarity index 100% rename from src/SLCore/Common/Models/FileUri.cs rename to src/Core/Helpers/FileUri.cs diff --git a/src/SLCore/Protocol/FileUriConverter.cs b/src/Core/Helpers/FileUriConverter.cs similarity index 100% rename from src/SLCore/Protocol/FileUriConverter.cs rename to src/Core/Helpers/FileUriConverter.cs diff --git a/src/Core/ILanguageProvider.cs b/src/Core/ILanguageProvider.cs index f0a7e790ef..b6d69a9369 100644 --- a/src/Core/ILanguageProvider.cs +++ b/src/Core/ILanguageProvider.cs @@ -28,7 +28,7 @@ public interface ILanguageProvider IReadOnlyList NonRoslynLanguages { get; } - IReadOnlyList RoslynLanguages { get; } + IReadOnlyList RoslynLanguages { get; } IReadOnlyList LanguagesInStandaloneMode { get; } @@ -56,7 +56,7 @@ public LanguageProvider() } public IReadOnlyList NonRoslynLanguages { get; } = [Language.C, Language.Cpp, Language.Js, Language.Ts, Language.Css, Language.Secrets, Language.Html, Language.TSql, Language.Text]; - public IReadOnlyList RoslynLanguages { get; } = [Language.CSharp, Language.VBNET]; + public IReadOnlyList RoslynLanguages { get; } = [Language.CSharp, Language.VBNET]; public IReadOnlyList AllKnownLanguages { get; } public IReadOnlyList LanguagesInStandaloneMode { get; } public IReadOnlyList ExtraLanguagesInConnectedMode { get; } = [Language.TSql, Language.Text]; diff --git a/src/Core/IServerBranchProvider.cs b/src/Core/IServerBranchProvider.cs deleted file mode 100644 index ed0f4ef2ef..0000000000 --- a/src/Core/IServerBranchProvider.cs +++ /dev/null @@ -1,46 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -namespace SonarLint.VisualStudio.Core -{ - public interface IServerBranchProvider - { - /// - /// Returns the Sonar server branch to use when requesting data - /// - /// The Sonar server branch name, - /// or the name of the Sonar server branch marked as "Main" if the branch cannot be determined, - /// or null if we are not in connected mode. - /// - /// - /// Only applies in connected mode. - /// - Task GetServerBranchNameAsync(CancellationToken token); - } - - /// - /// Stateful version of - /// - /// The implementation is a singleton that recalculates the Sonar branch name - /// automatically when necessary e.g. when a different solution is opened - /// - public interface IStatefulServerBranchProvider : IServerBranchProvider { } - -} diff --git a/src/Core/Language.cs b/src/Core/Language.cs index 1eea8d9b55..95c19617f2 100644 --- a/src/Core/Language.cs +++ b/src/Core/Language.cs @@ -35,11 +35,12 @@ namespace SonarLint.VisualStudio.Core /// [DebuggerDisplay("{Name} (ID: {Id})")] [TypeConverter(typeof(LanguageConverter))] - public sealed class Language : IEquatable + public class Language : IEquatable { private const string VersionNumberPattern = "(\\d+\\.\\d+\\.\\d+\\.\\d+)\\"; - private static readonly PluginInfo CSharpPlugin = new("csharpenterprise", $"sonar-csharp-enterprise-plugin-{VersionNumberPattern}.jar"); - private static readonly PluginInfo VbNetPlugin = new("vbnetenterprise", $"sonar-vbnet-enterprise-plugin-{VersionNumberPattern}.jar"); + private static readonly PluginInfo SqvsRoslynPlugin = new("sqvsroslyn", $"sonarqube-ide-visualstudio-roslyn-plugin-{VersionNumberPattern}.jar"); + private static readonly PluginInfo CSharpPlugin = new("csharpenterprise", $"sonar-csharp-enterprise-plugin-{VersionNumberPattern}.jar", isEnabledForAnalysis: false); + private static readonly PluginInfo VbNetPlugin = new("vbnetenterprise", $"sonar-vbnet-enterprise-plugin-{VersionNumberPattern}.jar", isEnabledForAnalysis: false); private static readonly PluginInfo SecretsPlugin = new("text", $"sonar-text-plugin-{VersionNumberPattern}.jar"); private static readonly PluginInfo CFamilyPlugin = new("cpp", $"sonar-cfamily-plugin-{VersionNumberPattern}.jar"); private static readonly PluginInfo JavascriptPlugin = new("javascript", $"sonar-javascript-plugin-{VersionNumberPattern}.jar"); @@ -62,9 +63,10 @@ public sealed class Language : IEquatable private static readonly RepoInfo TsqlRepo = new("tsql"); public static readonly Language Unknown = new(); - public static readonly Language CSharp = new("CSharp", CoreStrings.CSharpLanguageName, "cs", CSharpPlugin, CSharpRepo, CSharpSecurityRepo, - settingsFileName: "sonarlint_csharp.globalconfig"); - public static readonly Language VBNET = new("VB", CoreStrings.VBNetLanguageName, "vbnet", VbNetPlugin, VbNetRepo, settingsFileName: "sonarlint_vb.globalconfig"); + public static readonly RoslynLanguage CSharp = new("CSharp", CoreStrings.CSharpLanguageName, "cs", SqvsRoslynPlugin, CSharpRepo, + settingsFileName: "sonarlint_csharp.globalconfig", roslynDllIdentifier: ".CSharp.", CSharpSecurityRepo, additionalPlugins: [CSharpPlugin]); + public static readonly RoslynLanguage VBNET = new("VB", CoreStrings.VBNetLanguageName, "vbnet", SqvsRoslynPlugin, VbNetRepo, settingsFileName: "sonarlint_vb.globalconfig", roslynDllIdentifier: ".VisualBasic.", + additionalPlugins: [VbNetPlugin]); public static readonly Language Cpp = new("C++", CoreStrings.CppLanguageName, "cpp", CFamilyPlugin, CppRepo); public static readonly Language C = new("C", "C", "c", CFamilyPlugin, CRepo); public static readonly Language Js = new("Js", "JavaScript", "js", JavascriptPlugin, JsRepo, JsSecurityRepo); @@ -96,10 +98,9 @@ public sealed class Language : IEquatable public string Name { get; } /// - /// Suffix and extension added to the language-specific rules configuration file for the language + /// Additional plugins that should be installed for a language /// - /// e.g. for ruleset-based languages this will be a language identifier + ".globalconfig" - public string SettingsFileNameAndExtension { get; } + public PluginInfo[] AdditionalPlugins { get; } public RepoInfo RepoInfo { get; } @@ -115,7 +116,6 @@ private Language() { Id = string.Empty; Name = CoreStrings.UnknownLanguageName; - SettingsFileNameAndExtension = string.Empty; } public Language( @@ -125,7 +125,7 @@ public Language( PluginInfo pluginInfo, RepoInfo repoInfo, RepoInfo securityRepoInfo = null, - string settingsFileName = null) + PluginInfo[] additionalPlugins = null) { if (string.IsNullOrWhiteSpace(id)) { @@ -139,7 +139,7 @@ public Language( Id = id; Name = name; - SettingsFileNameAndExtension = settingsFileName; + AdditionalPlugins = additionalPlugins; ServerLanguageKey = serverLanguageKey ?? throw new ArgumentNullException(nameof(serverLanguageKey)); PluginInfo = pluginInfo ?? throw new ArgumentNullException(nameof(pluginInfo)); RepoInfo = repoInfo ?? throw new ArgumentNullException(nameof(repoInfo)); @@ -175,4 +175,37 @@ public override int GetHashCode() public bool HasRepoKey(string repoKey) => RepoInfo.Key == repoKey || SecurityRepoInfo?.Key == repoKey; } + + /// + /// Represents a Roslyn-based programming language with specific Roslyn analyzer configuration. + /// + public class RoslynLanguage : Language + { + /// + /// Suffix and extension added to the language-specific rules configuration file for the language + /// + /// e.g. for ruleset-based languages this will be a language identifier + ".globalconfig" + public string SettingsFileNameAndExtension { get; } + + /// + /// A substring that is contained in the name of the analyzer files for specific roslyn language. + /// + public string RoslynDllIdentifier { get; } + + public RoslynLanguage( + string id, + string name, + string serverLanguageKey, + PluginInfo pluginInfo, + RepoInfo repoInfo, + string settingsFileName, + string roslynDllIdentifier, + RepoInfo securityRepoInfo = null, + PluginInfo[] additionalPlugins = null) + : base(id, name, serverLanguageKey, pluginInfo, repoInfo, securityRepoInfo, additionalPlugins) + { + SettingsFileNameAndExtension = settingsFileName; + RoslynDllIdentifier = roslynDllIdentifier; + } + } } diff --git a/src/Core/PluginInfo.cs b/src/Core/PluginInfo.cs index 3d8460077f..0b095c2ad4 100644 --- a/src/Core/PluginInfo.cs +++ b/src/Core/PluginInfo.cs @@ -22,10 +22,11 @@ namespace SonarLint.VisualStudio.Core; public record PluginInfo { - public PluginInfo(string pluginKey, string filePattern) + public PluginInfo(string pluginKey, string filePattern, bool isEnabledForAnalysis = true) { Key = pluginKey ?? throw new ArgumentNullException(nameof(pluginKey)); FilePattern = filePattern; + IsEnabledForAnalysis = isEnabledForAnalysis; } public string Key { get; } @@ -34,4 +35,6 @@ public PluginInfo(string pluginKey, string filePattern) /// Nullable, because it can be a connected mode only language so there will be no file on disk (plugin won't be embedded) /// public string FilePattern { get; } + + public bool IsEnabledForAnalysis { get; } } diff --git a/src/Core/SonarCompositeRuleId.cs b/src/Core/SonarCompositeRuleId.cs index 9fee268340..608b5e5f07 100644 --- a/src/Core/SonarCompositeRuleId.cs +++ b/src/Core/SonarCompositeRuleId.cs @@ -47,10 +47,12 @@ public SonarCompositeRuleId(string repoKey, string ruleKey) { RepoKey = repoKey ?? throw new ArgumentNullException(nameof(repoKey)); RuleKey = ruleKey ?? throw new ArgumentNullException(nameof(ruleKey)); - ErrorListErrorCode = repoKey + Separator + ruleKey; + ErrorListErrorCode = GetFullErrorCode(repoKey, ruleKey); Language = LanguageProvider.Instance.AllKnownLanguages.FirstOrDefault(x => x.RepoInfo.Key == repoKey) ?? Language.Unknown; } + public static string GetFullErrorCode(string repoKey, string ruleKey) => repoKey + Separator + ruleKey; + public string ErrorListErrorCode { get; } public string RepoKey { get; } public string RuleKey { get; } diff --git a/src/Education.UnitTests/packages.lock.json b/src/Education.UnitTests/packages.lock.json index 89711facab..fc5956ddd8 100644 --- a/src/Education.UnitTests/packages.lock.json +++ b/src/Education.UnitTests/packages.lock.json @@ -122,16 +122,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -1329,8 +1319,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/EmbeddedSonarAnalyzer.props b/src/EmbeddedSonarAnalyzer.props index 23624d9d81..fa366e179b 100644 --- a/src/EmbeddedSonarAnalyzer.props +++ b/src/EmbeddedSonarAnalyzer.props @@ -9,8 +9,10 @@ 11.4.1.34873 3.19.0.5695 2.30.0.8328 + + 1.0.0.89 - 10.34.0.83431 + 10.35.0.83435 1.0.0 diff --git a/src/Infrastructure.VS.UnitTests/Roslyn/AnalyzerArrayComparerTests.cs b/src/Infrastructure.VS.UnitTests/Roslyn/AnalyzerArrayComparerTests.cs deleted file mode 100644 index 269f83911c..0000000000 --- a/src/Infrastructure.VS.UnitTests/Roslyn/AnalyzerArrayComparerTests.cs +++ /dev/null @@ -1,125 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using SonarLint.VisualStudio.Infrastructure.VS.Roslyn; - -namespace SonarLint.VisualStudio.Infrastructure.VS.UnitTests.Roslyn; - -[TestClass] -public class AnalyzerArrayComparerTests -{ - [TestMethod] - public void Equals_BothNulls_True() - { - AnalyzerArrayComparer.Instance.Equals(null, null).Should().BeTrue(); - } - - [TestMethod] - public void Equals_OneNull_False() - { - var analyzerArray = ImmutableArray.Create(); - AnalyzerArrayComparer.Instance.Equals(null, analyzerArray).Should().BeFalse(); - AnalyzerArrayComparer.Instance.Equals(analyzerArray, null).Should().BeFalse(); - } - - [TestMethod] - public void Equals_EmptyArrays_True() - { - var analyzerArray1 = ImmutableArray.Create(); - var analyzerArray2 = ImmutableArray.Create(); - AnalyzerArrayComparer.Instance.Equals(analyzerArray1, analyzerArray2).Should().BeTrue(); - AnalyzerArrayComparer.Instance.Equals(analyzerArray2, analyzerArray1).Should().BeTrue(); - } - - [TestMethod] - public void Equals_DifferentLengths_False() - { - var empty = ImmutableArray.Create(); - var single = ImmutableArray.Create(GetAnalyzerFileReference()); - var triple = ImmutableArray.Create(GetAnalyzerFileReference(), GetAnalyzerFileReference(), GetAnalyzerFileReference()); - AnalyzerArrayComparer.Instance.Equals(empty, single).Should().BeFalse(); - AnalyzerArrayComparer.Instance.Equals(single, empty).Should().BeFalse(); - AnalyzerArrayComparer.Instance.Equals(single, triple).Should().BeFalse(); - AnalyzerArrayComparer.Instance.Equals(triple, single).Should().BeFalse(); - } - - [TestMethod] - public void Equals_SameArray_True() - { - var array = ImmutableArray.Create(GetAnalyzerFileReference()); - AnalyzerArrayComparer.Instance.Equals(array, array).Should().BeTrue(); - } - - [TestMethod] - public void Equals_ArrayWithSameValuesInOrder_True() - { - var analyzerFileReference1 = GetAnalyzerFileReference(); - var analyzerFileReference2 = GetAnalyzerFileReference(); - var analyzerFileReference3 = GetAnalyzerFileReference(); - var array1 = ImmutableArray.Create(analyzerFileReference1, analyzerFileReference2, analyzerFileReference3); - var array2 = ImmutableArray.Create(analyzerFileReference1, analyzerFileReference2, analyzerFileReference3); - AnalyzerArrayComparer.Instance.Equals(array1, array2).Should().BeTrue(); - AnalyzerArrayComparer.Instance.Equals(array2, array1).Should().BeTrue(); - } - - [TestMethod] - public void Equals_ArrayWithSameValuesInDifferentOrder_False() - { - var analyzerFileReference1 = GetAnalyzerFileReference(); - var analyzerFileReference2 = GetAnalyzerFileReference(); - var analyzerFileReference3 = GetAnalyzerFileReference(); - var array1 = ImmutableArray.Create(analyzerFileReference1, analyzerFileReference3, analyzerFileReference2); - var array2 = ImmutableArray.Create(analyzerFileReference1, analyzerFileReference2, analyzerFileReference3); - AnalyzerArrayComparer.Instance.Equals(array1, array2).Should().BeFalse(); - AnalyzerArrayComparer.Instance.Equals(array2, array1).Should().BeFalse(); - } - - [TestMethod] - public void Equals_ArrayWithEquivalentValues_True() - { - var analyzerAssemblyLoader1 = Substitute.For(); - var analyzerAssemblyLoader2 = Substitute.For(); - var analyzerFileReference11 = GetAnalyzerFileReference(@"C:\analyzer1", analyzerAssemblyLoader1); - var analyzerFileReference12 = GetAnalyzerFileReference(@"C:\analyzer1", analyzerAssemblyLoader1); - var analyzerFileReference21 = GetAnalyzerFileReference(@"C:\analyzer2", analyzerAssemblyLoader2); - var analyzerFileReference22 = GetAnalyzerFileReference(@"C:\analyzer2", analyzerAssemblyLoader2); - var array1 = ImmutableArray.Create(analyzerFileReference11, analyzerFileReference21); - var array2 = ImmutableArray.Create(analyzerFileReference12, analyzerFileReference22); - AnalyzerArrayComparer.Instance.Equals(array1, array2).Should().BeTrue(); - AnalyzerArrayComparer.Instance.Equals(array2, array1).Should().BeTrue(); - } - - [TestMethod] - public void GetHashCode_DelegatesToObject() - { - ImmutableArray? nullArray = null; - AnalyzerArrayComparer.Instance.GetHashCode(nullArray).Should().Be(nullArray.GetHashCode()); - var analyzerFileReferences = ImmutableArray.Create(); - AnalyzerArrayComparer.Instance.GetHashCode(analyzerFileReferences).Should().Be(analyzerFileReferences.GetHashCode()); - } - - private AnalyzerFileReference GetAnalyzerFileReference(string filePath = @"C:\analyzer", IAnalyzerAssemblyLoader analyzerAssemblyLoader = null) - { - return new AnalyzerFileReference(filePath, analyzerAssemblyLoader ?? Substitute.For()); - } -} diff --git a/src/Infrastructure.VS.UnitTests/Roslyn/AnalyzerChangeTests.cs b/src/Infrastructure.VS.UnitTests/Roslyn/AnalyzerChangeTests.cs deleted file mode 100644 index 8b2eb5db22..0000000000 --- a/src/Infrastructure.VS.UnitTests/Roslyn/AnalyzerChangeTests.cs +++ /dev/null @@ -1,192 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using SonarLint.VisualStudio.Infrastructure.VS.Roslyn; - -namespace SonarLint.VisualStudio.Infrastructure.VS.UnitTests.Roslyn; - -[TestClass] -public class AnalyzerChangeTests -{ - private IRoslynSolutionWrapper solution; - private AnalyzerFileReference analyzer1; - private AnalyzerFileReference analyzer2; - private AnalyzerFileReference analyzer3; - private AnalyzerFileReference analyzer4; - - [TestInitialize] - public void TestInitialize() - { - solution = Substitute.For(); - - analyzer1 = new AnalyzerFileReference(@"C:\abc1", Substitute.For()); - analyzer2 = new AnalyzerFileReference(@"C:\abc2", Substitute.For()); - analyzer3 = new AnalyzerFileReference(@"C:\abc3", Substitute.For()); - analyzer4 = new AnalyzerFileReference(@"C:\abc4", Substitute.For()); - } - - [TestMethod] - public void Properties_SetToCorrectValues() - { - var analyzersToRemove = ImmutableArray.Create(analyzer1, analyzer2); - var analyzersToAdd = ImmutableArray.Create(analyzer3, analyzer4); - var testSubject = new AnalyzerChange(analyzersToRemove, analyzersToAdd); - - testSubject.AnalyzersToRemove.Should().BeEquivalentTo(analyzersToRemove); - testSubject.AnalyzersToAdd.Should().BeEquivalentTo(analyzersToAdd); - } - - [TestMethod] - public void Change_PerformsUpdatesOnSolution() - { - var intermediateSolution = Substitute.For(); - var resultingSolution = Substitute.For(); - var allAnalyzersToRemove = ImmutableArray.Create(analyzer1, analyzer2); - var allAnalyzersToAdd = ImmutableArray.Create(analyzer3, analyzer4); - SetUpRemove(allAnalyzersToRemove, allAnalyzersToRemove, solution, intermediateSolution); - SetUpAdd(allAnalyzersToAdd, allAnalyzersToAdd, intermediateSolution, resultingSolution); - var testSubject = new AnalyzerChange(allAnalyzersToRemove, allAnalyzersToAdd); - - testSubject.Change(solution).Should().BeSameAs(resultingSolution); - } - - [TestMethod] - public void Change_FiltersOutAlreadyContainedOrRemovedAnalyzers() - { - var intermediateSolution = Substitute.For(); - var resultingSolution = Substitute.For(); - var allAnalyzersToRemove = ImmutableArray.Create(analyzer1, analyzer2); - List actualAnalyzersToRemove = [analyzer1]; - var allAnalyzersToAdd = ImmutableArray.Create(analyzer3, analyzer4); - List actualAnalyzersToAdd = [analyzer4]; - SetUpRemove(allAnalyzersToRemove, actualAnalyzersToRemove, solution, intermediateSolution); - SetUpAdd(allAnalyzersToAdd, actualAnalyzersToAdd, intermediateSolution, resultingSolution); - var testSubject = new AnalyzerChange(allAnalyzersToRemove, allAnalyzersToAdd); - - testSubject.Change(solution).Should().BeSameAs(resultingSolution); - - Received.InOrder(() => - { - solution.ContainsAnalyzer(analyzer1); - solution.ContainsAnalyzer(analyzer2); - solution.RemoveAnalyzerReferences(Arg.Is>(x => x.SequenceEqual(actualAnalyzersToRemove))); - intermediateSolution.ContainsAnalyzer(analyzer3); - intermediateSolution.ContainsAnalyzer(analyzer4); - intermediateSolution.AddAnalyzerReferences(Arg.Is>(x => x.SequenceEqual(actualAnalyzersToAdd))); - }); - } - - [TestMethod] - public void Change_RemovedAnalyzersAlreadyRemoved_SkipsRemoval() - { - var resultingSolution = Substitute.For(); - var allAnalyzersToRemove = ImmutableArray.Create(analyzer1); - SetUpRemove(allAnalyzersToRemove, [], solution, default); - var allAnalyzersToAdd = ImmutableArray.Create(analyzer2); - List actualAnalyzersToAdd = [analyzer2]; - SetUpAdd(allAnalyzersToAdd, actualAnalyzersToAdd, solution, resultingSolution); - var testSubject = new AnalyzerChange(allAnalyzersToRemove, - allAnalyzersToAdd); - - testSubject.Change(solution).Should().BeSameAs(resultingSolution); - - solution.DidNotReceiveWithAnyArgs().RemoveAnalyzerReferences(default); - } - - - [TestMethod] - public void Change_NothingToRemove_SkipsRemoval() - { - var resultingSolution = Substitute.For(); - SetUpRemove(ImmutableArray.Empty, [], solution, default); - var allAnalyzersToAdd = ImmutableArray.Create(analyzer2); - List actualAnalyzersToAdd = [analyzer2]; - SetUpAdd(allAnalyzersToAdd, actualAnalyzersToAdd, solution, resultingSolution); - var testSubject = new AnalyzerChange(ImmutableArray.Empty, allAnalyzersToAdd); - - testSubject.Change(solution).Should().BeSameAs(resultingSolution); - - solution.DidNotReceiveWithAnyArgs().RemoveAnalyzerReferences(default); - } - - [TestMethod] - public void Change_AddedAnalyzersAlreadyAdded_SkipsAddition() - { - var resultingSolution = Substitute.For(); - var allAnalyzersToRemove = ImmutableArray.Create(analyzer1); - SetUpRemove(allAnalyzersToRemove, [analyzer1], solution, resultingSolution); - var allAnalyzersToAdd = ImmutableArray.Create(analyzer2); - SetUpAdd(allAnalyzersToAdd, [], resultingSolution, default); - - var testSubject = new AnalyzerChange(allAnalyzersToRemove, - allAnalyzersToAdd); - - testSubject.Change(solution).Should().BeSameAs(resultingSolution); - - solution.DidNotReceiveWithAnyArgs().AddAnalyzerReferences(default); - } - - - [TestMethod] - public void Change_NothingToAdd_SkipsAddition() - { - var resultingSolution = Substitute.For(); - var allAnalyzersToRemove = ImmutableArray.Create(analyzer1); - SetUpRemove(allAnalyzersToRemove, [analyzer1], solution, resultingSolution); - SetUpAdd(ImmutableArray.Empty, [], solution, default); - var testSubject = new AnalyzerChange(allAnalyzersToRemove, - ImmutableArray.Empty); - - testSubject.Change(solution).Should().BeSameAs(resultingSolution); - - solution.DidNotReceiveWithAnyArgs().AddAnalyzerReferences(default); - } - - private static void SetUpAdd( - ImmutableArray allAnalyzersToAdd, - ICollection actualAnalyzersToAdd, - IRoslynSolutionWrapper original, - IRoslynSolutionWrapper resulting) - { - SetUpContains(allAnalyzersToAdd, allAnalyzersToAdd.Except(actualAnalyzersToAdd).ToList(), original); - original.AddAnalyzerReferences(Arg.Is>(x => x.SequenceEqual(actualAnalyzersToAdd))).Returns(resulting); - } - - private static void SetUpRemove( - ImmutableArray allAnalyzersToRemove, - ICollection actualAnalyzersToRemove, - IRoslynSolutionWrapper original, - IRoslynSolutionWrapper resulting) - { - SetUpContains(allAnalyzersToRemove, actualAnalyzersToRemove, original); - original.RemoveAnalyzerReferences(Arg.Is>(x => x.SequenceEqual(actualAnalyzersToRemove))).Returns(resulting); - } - - private static void SetUpContains(IEnumerable allAnalyzers, ICollection containedAnalyzers, IRoslynSolutionWrapper solutionWrapper) - { - foreach (var analyzer in allAnalyzers) - { - solutionWrapper.ContainsAnalyzer(analyzer).Returns(containedAnalyzers.Contains(analyzer)); - } - } -} diff --git a/src/Infrastructure.VS.UnitTests/Roslyn/EmbeddedDotnetAnalyzerProviderTests.cs b/src/Infrastructure.VS.UnitTests/Roslyn/EmbeddedDotnetAnalyzerProviderTests.cs deleted file mode 100644 index e85d35f77e..0000000000 --- a/src/Infrastructure.VS.UnitTests/Roslyn/EmbeddedDotnetAnalyzerProviderTests.cs +++ /dev/null @@ -1,237 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Immutable; -using System.IO; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.ConfigurationScope; -using SonarLint.VisualStudio.Infrastructure.VS.Roslyn; - -namespace SonarLint.VisualStudio.Infrastructure.VS.UnitTests.Roslyn; - -[TestClass] -public class EmbeddedDotnetAnalyzerProviderTests -{ - private const string AnalyzersPath = "C:\\somepath"; - private readonly IAnalyzerAssemblyLoader analyzerAssemblyLoader = Substitute.For(); - private EmbeddedDotnetAnalyzerProvider testSubject; - private IEmbeddedDotnetAnalyzersLocator locator; - private IAnalyzerAssemblyLoaderFactory loaderFactory; - private IConfigurationScopeDotnetAnalyzerIndicator indicator; - private ILogger logger; - private IThreadHandling threadHandling; - - [TestInitialize] - public void TestInitialize() - { - locator = Substitute.For(); - loaderFactory = Substitute.For(); - loaderFactory.Create().Returns(analyzerAssemblyLoader); - logger = Substitute.For(); - indicator = Substitute.For(); - threadHandling = new NoOpThreadHandler(); - - testSubject = new EmbeddedDotnetAnalyzerProvider(locator, loaderFactory, indicator, logger, threadHandling); - MockServices(); - } - - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void MefCtor_IsSingleton() - { - MefTestHelpers.CheckIsSingletonMefComponent(); - } - - [TestMethod] - public void Ctor_CreatesLoader() - { - var factory = Substitute.For(); - - new EmbeddedDotnetAnalyzerProvider(default, factory, default, default, default); - - factory.Received(1).Create(); - } - - [TestMethod] - public void GetBasicAsync_GetsAnalyzersFromExpectedLocation() - { - testSubject.GetBasicAsync(); - - locator.Received(1).GetBasicAnalyzerFullPaths(); - } - - [TestMethod] - public void GetBasicAsync_RunsOnBackgroundThread() - { - var threadHandlingMock = Substitute.For(); - var subject = new EmbeddedDotnetAnalyzerProvider(default, - Substitute.For(), - default, - default, - threadHandlingMock); - - subject.GetBasicAsync(); - - threadHandlingMock.Received(1).RunOnBackgroundThread(Arg.Any>>>()); - } - - [TestMethod] - public void GetEnterpriseOrNullAsync_GetsAnalyzersFromExpectedLocation() - { - testSubject.GetEnterpriseOrNullAsync("scope"); - - locator.Received(1).GetEnterpriseAnalyzerFullPaths(); - } - - [TestMethod] - public void GetEnterpriseOrNullAsync_RunsOnBackgroundThread() - { - var threadHandlingMock = Substitute.For(); - var subject = new EmbeddedDotnetAnalyzerProvider(default, - Substitute.For(), - default, - default, - threadHandlingMock); - - subject.GetEnterpriseOrNullAsync("scope"); - - threadHandlingMock.Received(1).RunOnBackgroundThread(Arg.Any?>>>()); - } - - [TestMethod] - public async Task GetBasicAsync_AnalyzerFilesExist_ReturnsAnalyzerFileReference() - { - locator.GetBasicAnalyzerFullPaths().Returns([GetAnalyzerFullPath("analyzer1.dll"), GetAnalyzerFullPath("analyzer2.dll")]); - - var analyzerFileReferences = await testSubject.GetBasicAsync(); - - analyzerFileReferences.Should().NotBeNull(); - analyzerFileReferences.Length.Should().Be(2); - ContainsExpectedAnalyzerFileReference(analyzerFileReferences, GetAnalyzerFullPath("analyzer1.dll")); - ContainsExpectedAnalyzerFileReference(analyzerFileReferences, GetAnalyzerFullPath("analyzer2.dll")); - } - - [TestMethod] - public async Task GetEnterpriseOrNullAsync_AnalyzerFilesExist_ReturnsAnalyzerFileReference() - { - const string configurationScopeId = "scope"; - locator.GetEnterpriseAnalyzerFullPaths().Returns([GetAnalyzerFullPath("analyzer1.dll"), GetAnalyzerFullPath("analyzer2.dll")]); - indicator.ShouldUseEnterpriseCSharpAnalyzerAsync(configurationScopeId).Returns(true); - - var analyzerFileReferences = await testSubject.GetEnterpriseOrNullAsync(configurationScopeId); - - analyzerFileReferences.Should().NotBeNull(); - analyzerFileReferences!.Value.Length.Should().Be(2); - ContainsExpectedAnalyzerFileReference(analyzerFileReferences.Value, GetAnalyzerFullPath("analyzer1.dll")); - ContainsExpectedAnalyzerFileReference(analyzerFileReferences.Value, GetAnalyzerFullPath("analyzer2.dll")); - } - - [TestMethod] - public async Task GetEnterpriseOrNullAsync_AnalyzerFilesExist_NotEnabledForConfigScope_ReturnsAnalyzerFileReference() - { - const string configurationScopeId = "scope"; - locator.GetEnterpriseAnalyzerFullPaths().Returns([GetAnalyzerFullPath("analyzer1.dll"), GetAnalyzerFullPath("analyzer2.dll")]); - indicator.ShouldUseEnterpriseCSharpAnalyzerAsync(configurationScopeId).Returns(false); - - var analyzerFileReferences = await testSubject.GetEnterpriseOrNullAsync(configurationScopeId); - - analyzerFileReferences.Should().BeNull(); - } - - [TestMethod] - public async Task GetBasicAsync_AnalyzerFilesDoNotExist_ReturnsLogsAndThrows() - { - locator.GetBasicAnalyzerFullPaths().Returns([]); - - Func act = () => testSubject.GetBasicAsync(); - - await act.Should().ThrowAsync().WithMessage(Resources.EmbeddedRoslynAnalyzersNotFound); - logger.Received(1).LogVerbose(Resources.EmbeddedRoslynAnalyzersNotFound); - } - - [TestMethod] - public async Task GetEnterpriseOrNullAsync_AnalyzerFilesDoNotExist_ReturnsLogsAndThrows() - { - locator.GetEnterpriseAnalyzerFullPaths().Returns([]); - - Func act = () => testSubject.GetEnterpriseOrNullAsync("scope"); - - await act.Should().ThrowAsync().WithMessage(Resources.EmbeddedRoslynAnalyzersNotFound); - logger.Received(1).LogVerbose(Resources.EmbeddedRoslynAnalyzersNotFound); - } - - [TestMethod] - public async Task GetBasicAsync_CachesAnalyzerFileReferences() - { - await testSubject.GetBasicAsync(); - await testSubject.GetBasicAsync(); - - loaderFactory.Received(1).Create(); - locator.Received(1).GetBasicAnalyzerFullPaths(); - } - - [TestMethod] - public async Task GetEnterpriseOrNullAsync_CachesAnalyzerFileReferences() - { - await testSubject.GetEnterpriseOrNullAsync("scope"); - await testSubject.GetEnterpriseOrNullAsync("other scope"); - - loaderFactory.Received(1).Create(); - locator.Received(1).GetEnterpriseAnalyzerFullPaths(); - } - - private static string GetAnalyzerFullPath(string analyzerFile) - { - return Path.Combine(AnalyzersPath, analyzerFile); - } - - private static void ContainsExpectedAnalyzerFileReference( - ImmutableArray analyzerFileReference, - string analyzerPath) - { - analyzerFileReference.Should().Contain(analyzerFile => analyzerFile.FullPath == analyzerPath); - } - - private void MockServices() - { - indicator.ShouldUseEnterpriseCSharpAnalyzerAsync(default).ReturnsForAnyArgs(true); - locator.GetBasicAnalyzerFullPaths().Returns([GetAnalyzerFullPath("analyzer1.dll")]); - locator.GetEnterpriseAnalyzerFullPaths().Returns([GetAnalyzerFullPath("analyzer1.dll")]); - loaderFactory.Create().Returns(analyzerAssemblyLoader); - } -} diff --git a/src/Infrastructure.VS.UnitTests/Roslyn/RoslynWorkspaceWrapperTests.cs b/src/Infrastructure.VS.UnitTests/Roslyn/RoslynWorkspaceWrapperTests.cs deleted file mode 100644 index 9f289b9e2e..0000000000 --- a/src/Infrastructure.VS.UnitTests/Roslyn/RoslynWorkspaceWrapperTests.cs +++ /dev/null @@ -1,174 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Infrastructure.VS.Roslyn; - -namespace SonarLint.VisualStudio.Infrastructure.VS.UnitTests.Roslyn; - -[TestClass] -public class RoslynWorkspaceWrapperTests -{ - private IThreadHandling threadHandling; - private AdhocWorkspace currentWorkspace; - private IAnalyzerChange analyzerChange; - private RoslynWorkspaceWrapper testSubject; - - [TestInitialize] - public void TestInitialize() - { - threadHandling = Substitute.For(); - threadHandling.RunOnUIThreadAsync(Arg.Any()).ReturnsForAnyArgs(info => - { - (info[0] as Action)?.Invoke(); - return Task.CompletedTask; - }); - currentWorkspace = new AdhocWorkspace(); - var slnInfo = SolutionInfo.Create(SolutionId.CreateNewId(), VersionStamp.Default, null, []); - currentWorkspace.AddSolution(slnInfo); - analyzerChange = Substitute.For(); - testSubject = new RoslynWorkspaceWrapper(currentWorkspace, threadHandling); - } - - [TestMethod] - public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); - - [TestMethod] - public async Task TryApplyChangesAsync_Success_UpdatesWorkspace() - { - var originalSolution = currentWorkspace.CurrentSolution; - var updatedSolutionWrapper = Substitute.For(); - var solutionThatSuccessfullyUpdates = GetSolutionThatSuccessfullyUpdates(originalSolution); - updatedSolutionWrapper.RoslynSolution.Returns(solutionThatSuccessfullyUpdates); - analyzerChange.Change(Arg.Is(x => x.RoslynSolution == originalSolution)).Returns(updatedSolutionWrapper); - - var result = await testSubject.TryApplyChangesAsync(analyzerChange); - - result.Should().NotBeNull(); - result.RoslynSolution.Should().NotBe(originalSolution); - result.RoslynSolution.Should().BeSameAs(currentWorkspace.CurrentSolution); - AssertCurrentSolutionIsEquivalentTo(updatedSolutionWrapper); - CheckRunChangeOnUIThread(); - } - - [TestMethod] - public async Task TryApplyChangesAsync_NoUpdate_ReturnsOriginal() - { - var originalSolution = currentWorkspace.CurrentSolution; - analyzerChange.Change(Arg.Is(x => x.RoslynSolution == originalSolution)).Returns(info => info[0] as IRoslynSolutionWrapper); - - var result = await testSubject.TryApplyChangesAsync(analyzerChange); - - result.Should().NotBeNull(); - result.RoslynSolution.Should().BeSameAs(originalSolution); - testSubject.CurrentSolution.RoslynSolution.Should().BeSameAs(originalSolution); - CheckRunChangeOnUIThread(); - } - - [TestMethod] - public async Task TryApplyChangesAsync_UpdateFails_RetriesAndFails() - { - var originalSolution = currentWorkspace.CurrentSolution; - var failed = new RoslynSolutionWrapper(GetSolutionThatFailsUpdate()); - analyzerChange.Change(Arg.Is(x => x.RoslynSolution == originalSolution)).Returns(failed); - - var result = await testSubject.TryApplyChangesAsync(analyzerChange); - - result.Should().BeNull(); - testSubject.CurrentSolution.RoslynSolution.Should().BeSameAs(originalSolution); - - Received.InOrder(() => - { - for (var i = 0; i < 5; i++) - { - threadHandling.RunOnUIThreadAsync(Arg.Any()); - analyzerChange.Change(Arg.Any()); - } - }); - } - - [DataRow(1)] - [DataRow(2)] - [DataRow(3)] - [DataRow(4)] - [DataTestMethod] - public async Task TryApplyChangesAsync(int failedUpdateTimes) - { - var originalSolution = currentWorkspace.CurrentSolution; - var failed = new RoslynSolutionWrapper(GetSolutionThatFailsUpdate()); - var success = new RoslynSolutionWrapper(GetSolutionThatSuccessfullyUpdates(originalSolution)); - analyzerChange.Change(Arg.Is(x => x.RoslynSolution == originalSolution)).Returns( - failed, - Enumerable.Repeat(failed, failedUpdateTimes - 1).Append(success).ToArray()); - - var result = await testSubject.TryApplyChangesAsync(analyzerChange); - - result.Should().NotBeNull(); - result.RoslynSolution.Should().NotBe(originalSolution); - result.RoslynSolution.Should().BeSameAs(currentWorkspace.CurrentSolution); - AssertCurrentSolutionIsEquivalentTo(success); - - Received.InOrder(() => - { - for (var i = 0; i < failedUpdateTimes + 1; i++) - { - threadHandling.RunOnUIThreadAsync(Arg.Any()); - analyzerChange.Change(Arg.Any()); - } - }); - } - - [TestMethod] - public void CurrentSolution_WorkspaceUpdated_ReturnsUpToDateValue() - { - var originalSolution = currentWorkspace.CurrentSolution; - var solutionThatSuccessfullyUpdates = GetSolutionThatSuccessfullyUpdates(originalSolution); - currentWorkspace.TryApplyChanges(solutionThatSuccessfullyUpdates).Should().BeTrue(); - var updatedSolution = currentWorkspace.CurrentSolution; - - var result = testSubject.CurrentSolution.RoslynSolution; - - result.Should().NotBeSameAs(originalSolution); - result.Should().BeSameAs(updatedSolution); - } - - private void CheckRunChangeOnUIThread() => - Received.InOrder(() => - { - threadHandling.RunOnUIThreadAsync(Arg.Any()); - analyzerChange.Change(Arg.Any()); - }); - - private static Solution GetSolutionThatFailsUpdate() => new AdhocWorkspace().AddSolution(SolutionInfo.Create(SolutionId.CreateNewId(), VersionStamp.Default, null, [])); - - private static Solution GetSolutionThatSuccessfullyUpdates(Solution originalSolution) - { - var analyzerAssemblyLoader = Substitute.For(); - var analyzerFileReference = new AnalyzerFileReference(@"C:\some\file", analyzerAssemblyLoader); - var updatedSolution = originalSolution.AddAnalyzerReference(analyzerFileReference); - return updatedSolution; - } - - private void AssertCurrentSolutionIsEquivalentTo(IRoslynSolutionWrapper solution) => - solution.Should().NotBeNull().And.BeAssignableTo().Subject.RoslynSolution.AnalyzerReferences.Should() - .BeEquivalentTo(testSubject.CurrentSolution.RoslynSolution.AnalyzerReferences); -} diff --git a/src/Infrastructure.VS.UnitTests/Roslyn/SolutionRoslynAnalyzerManagerTests.cs b/src/Infrastructure.VS.UnitTests/Roslyn/SolutionRoslynAnalyzerManagerTests.cs deleted file mode 100644 index d1004fa508..0000000000 --- a/src/Infrastructure.VS.UnitTests/Roslyn/SolutionRoslynAnalyzerManagerTests.cs +++ /dev/null @@ -1,381 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using NSubstitute.ClearExtensions; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.ConfigurationScope; -using SonarLint.VisualStudio.Core.Synchronization; -using SonarLint.VisualStudio.Infrastructure.VS.Roslyn; - -namespace SonarLint.VisualStudio.Infrastructure.VS.UnitTests.Roslyn; - -[TestClass] -public class SolutionRoslynAnalyzerManagerTests -{ - private static readonly IEqualityComparer DefaultComparer = EqualityComparer.Default; - private IBasicRoslynAnalyzerProvider basicRoslynAnalyzerProvider; - private IEnterpriseRoslynAnalyzerProvider enterpriseRoslynAnalyzerProvider; - private IRoslynWorkspaceWrapper roslynWorkspaceWrapper; - private IActiveConfigScopeTracker activeConfigScopeTracker; - private IActiveSolutionTracker activeSolutionTracker; - private IAsyncLockFactory asyncLockFactory; - private IAsyncLock asyncLock; - private IEqualityComparer?> analyzerComparer; - private TestLogger logger; - private SolutionRoslynAnalyzerManager testSubject; - private readonly ImmutableArray embeddedAnalyzers = ImmutableArray.Create(new AnalyzerFileReference(@"C:\path\embedded", Substitute.For())); - private readonly ImmutableArray connectedAnalyzers = - ImmutableArray.Create( - new AnalyzerFileReference(@"C:\path\connected1", Substitute.For()), - new AnalyzerFileReference(@"C:\path\connected2", Substitute.For())); - - [TestInitialize] - public void TestInitialize() - { - logger = new TestLogger(); - basicRoslynAnalyzerProvider = Substitute.For(); - enterpriseRoslynAnalyzerProvider = Substitute.For(); - roslynWorkspaceWrapper = Substitute.For(); - analyzerComparer = Substitute.For?>>(); - activeConfigScopeTracker = Substitute.For(); - activeSolutionTracker = Substitute.For(); - asyncLockFactory = Substitute.For(); - asyncLock = Substitute.For(); - asyncLockFactory.Create().Returns(asyncLock); - - testSubject = new SolutionRoslynAnalyzerManager( - basicRoslynAnalyzerProvider, - enterpriseRoslynAnalyzerProvider, - roslynWorkspaceWrapper, - analyzerComparer, - activeConfigScopeTracker, - activeSolutionTracker, - asyncLockFactory, - logger); - } - - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void MefCtor_CheckIsSingleton() - { - MefTestHelpers.CheckIsSingletonMefComponent(); - } - - [TestMethod] - public void Ctor_SubscribesToEvents() - { - new SolutionRoslynAnalyzerManager( - basicRoslynAnalyzerProvider, - enterpriseRoslynAnalyzerProvider, - roslynWorkspaceWrapper, - analyzerComparer, - activeConfigScopeTracker, - activeSolutionTracker, - asyncLockFactory, - logger); - - // todo add more tests for the events - activeConfigScopeTracker.Received().CurrentConfigurationScopeChanged += Arg.Any>(); - activeSolutionTracker.Received().ActiveSolutionChanged += Arg.Any>(); - } - - [TestMethod] - public async Task OnSolutionStateChangedAsync_NoOpenSolution_DoesNothing() - { - await testSubject.OnSolutionStateChangedAsync(null); - - roslynWorkspaceWrapper.ReceivedCalls().Should().BeEmpty(); - } - - [TestMethod] - public async Task OnSolutionStateChangedAsync_AcquiresLock() - { - await testSubject.OnSolutionStateChangedAsync(null); - - await asyncLock.Received(1).AcquireAsync(); - } - - [TestMethod] - public async Task OnSolutionStateChangedAsync_StandaloneSolution_AppliesEmbeddedAnalyzer() - { - basicRoslynAnalyzerProvider.GetBasicAsync() - .Returns(embeddedAnalyzers); - var solution = Substitute.For(); - SetUpAnalyzerUpdate([], embeddedAnalyzers, solution); - - await testSubject.OnSolutionStateChangedAsync("solution"); - - roslynWorkspaceWrapper.Received().TryApplyChangesAsync(Arg.Is(x => x.AnalyzersToAdd.SequenceEqual(embeddedAnalyzers, DefaultComparer) && x.AnalyzersToRemove.Length == 0)); - AssertNoErrorsInLogs(); - } - - - [TestMethod] - public async Task OnSolutionStateChangedAsync_StandaloneSolution_UpdateFails_Logs() - { - basicRoslynAnalyzerProvider.GetBasicAsync() - .Returns(embeddedAnalyzers); - IRoslynSolutionWrapper failedUpdate = null; - SetUpAnalyzerUpdate([], embeddedAnalyzers, failedUpdate); - - await testSubject.OnSolutionStateChangedAsync("solution"); - - roslynWorkspaceWrapper.Received().TryApplyChangesAsync(Arg.Is(x => x.AnalyzersToAdd.SequenceEqual(embeddedAnalyzers, DefaultComparer) && x.AnalyzersToRemove.Length == 0)); - AssertUpdateFailedAndLogged(); - } - - [TestMethod] - public async Task OnSolutionStateChangedAsync_StandaloneSolution_BindingSet_RemovesStandaloneAndAppliesConnectedAnalyzer() - { - const string solutionName = "solution"; - await SetUpStandaloneSolution(solutionName); - analyzerComparer.Equals(embeddedAnalyzers, connectedAnalyzers).Returns(false); - enterpriseRoslynAnalyzerProvider.GetEnterpriseOrNullAsync(solutionName).Returns(connectedAnalyzers); - SetUpAnalyzerUpdate(embeddedAnalyzers, connectedAnalyzers, Substitute.For()); - - await testSubject.OnSolutionStateChangedAsync(solutionName); - - analyzerComparer.Received().Equals(embeddedAnalyzers, connectedAnalyzers); - roslynWorkspaceWrapper.Received().TryApplyChangesAsync(Arg.Is(x => - x.AnalyzersToAdd.SequenceEqual(connectedAnalyzers, DefaultComparer) && x.AnalyzersToRemove.SequenceEqual(embeddedAnalyzers, DefaultComparer))); - basicRoslynAnalyzerProvider.DidNotReceiveWithAnyArgs().GetBasicAsync(); - AssertNoErrorsInLogs(); - } - - [TestMethod] - public async Task OnSolutionStateChangedAsync_StandaloneSolution_BindingSet_NoConnectedAnalyzer_DoesNotReRegisterEmbedded() - { - const string solutionName = "solution"; - await SetUpStandaloneSolution(solutionName); - EnableDefaultEmbeddedAnalyzers(); - enterpriseRoslynAnalyzerProvider.GetEnterpriseOrNullAsync(solutionName).Returns((ImmutableArray?)null); - analyzerComparer.Equals(embeddedAnalyzers, embeddedAnalyzers).Returns(true); - - await testSubject.OnSolutionStateChangedAsync(solutionName); - - Received.InOrder(() => - { - enterpriseRoslynAnalyzerProvider.GetEnterpriseOrNullAsync(solutionName); - basicRoslynAnalyzerProvider.GetBasicAsync(); - analyzerComparer.Equals(embeddedAnalyzers, embeddedAnalyzers); - }); - roslynWorkspaceWrapper.DidNotReceiveWithAnyArgs().TryApplyChangesAsync(default); - AssertNoErrorsInLogs(); - } - - [TestMethod] - public async Task OnSolutionStateChangedAsync_SolutionClosedAndReopened_RegistersAnalyzersAgain() - { - const string solutionName = "solution"; - await SetUpStandaloneSolution(solutionName); - EnableDefaultEmbeddedAnalyzers(); - SetUpAnalyzerUpdate(embeddedAnalyzers, [], Substitute.For()); - SetUpAnalyzerUpdate([], embeddedAnalyzers, Substitute.For()); - - await testSubject.OnSolutionStateChangedAsync(null); - await testSubject.OnSolutionStateChangedAsync(solutionName); - - Received.InOrder(() => - { - roslynWorkspaceWrapper.TryApplyChangesAsync(Arg.Is(x => x.AnalyzersToAdd.Length == 0 && x.AnalyzersToRemove.SequenceEqual(embeddedAnalyzers, DefaultComparer))); - enterpriseRoslynAnalyzerProvider.GetEnterpriseOrNullAsync(solutionName); - basicRoslynAnalyzerProvider.GetBasicAsync(); - roslynWorkspaceWrapper.TryApplyChangesAsync(Arg.Is(x => x.AnalyzersToAdd.SequenceEqual(embeddedAnalyzers, DefaultComparer) && x.AnalyzersToRemove.Length == 0)); - }); - AssertNoErrorsInLogs(); - } - - [TestMethod] - public async Task OnSolutionStateChangedAsync_SolutionClosedAndReopenedAsBound_RegistersConnectedAnalyzers() - { - const string solutionName = "solution"; - await SetUpStandaloneSolution(solutionName); - enterpriseRoslynAnalyzerProvider.GetEnterpriseOrNullAsync(solutionName).Returns(connectedAnalyzers); - - SetUpAnalyzerUpdate(embeddedAnalyzers, [], Substitute.For()); - SetUpAnalyzerUpdate([], connectedAnalyzers, Substitute.For()); - - await testSubject.OnSolutionStateChangedAsync(null); - await testSubject.OnSolutionStateChangedAsync(solutionName); - - Received.InOrder(() => - { - roslynWorkspaceWrapper.TryApplyChangesAsync(Arg.Is(x => x.AnalyzersToAdd.Length == 0 && x.AnalyzersToRemove.SequenceEqual(embeddedAnalyzers, DefaultComparer))); - enterpriseRoslynAnalyzerProvider.GetEnterpriseOrNullAsync(solutionName); - roslynWorkspaceWrapper.TryApplyChangesAsync(Arg.Is(x => x.AnalyzersToAdd.SequenceEqual(connectedAnalyzers, DefaultComparer) && x.AnalyzersToRemove.Length == 0)); - }); - AssertNoErrorsInLogs(); - } - - [TestMethod] - public async Task OnSolutionStateChangedAsync_DifferentSolutionOpened_RegistersAnalyzers() - { - await SetUpStandaloneSolution("original solution"); - var differentSolution = "different solution"; - EnableDefaultEmbeddedAnalyzers(); - - enterpriseRoslynAnalyzerProvider.GetEnterpriseOrNullAsync(differentSolution).Returns(connectedAnalyzers); - SetUpAnalyzerUpdate(embeddedAnalyzers, connectedAnalyzers, Substitute.For()); - - await testSubject.OnSolutionStateChangedAsync(differentSolution); - - roslynWorkspaceWrapper.TryApplyChangesAsync(Arg.Is(x => - x.AnalyzersToRemove.SequenceEqual(embeddedAnalyzers, DefaultComparer) && x.AnalyzersToAdd.SequenceEqual(connectedAnalyzers, DefaultComparer))); - AssertNoErrorsInLogs(); - } - - [TestMethod] - public void Dispose_UnsubscribesFromEvents() - { - testSubject.Dispose(); - - activeConfigScopeTracker.Received().CurrentConfigurationScopeChanged -= Arg.Any>(); - activeSolutionTracker.Received().ActiveSolutionChanged -= Arg.Any>(); - } - - [TestMethod] - public void OnSolutionStateChangedAsync_Disposed_Throws() - { - var act = async () => await testSubject.OnSolutionStateChangedAsync("solution"); - testSubject.Dispose(); - - act.Should().Throw(); - } - - [TestMethod] - public async Task OnSolutionStateChangedAsync_SolutionClosed_RemovesAnalzyers() - { - const string solutionName = "solution"; - await SetUpStandaloneSolution(solutionName); - SetUpAnalyzerUpdate(embeddedAnalyzers, [], Substitute.For()); - - await testSubject.OnSolutionStateChangedAsync(null); - - await enterpriseRoslynAnalyzerProvider.DidNotReceiveWithAnyArgs().GetEnterpriseOrNullAsync(solutionName); - AssertNoErrorsInLogs(); - } - - - [TestMethod] - public async Task OnSolutionStateChangedAsync_SolutionClosed_FailedToRemove_Logs() - { - const string solutionName = "solution"; - await SetUpStandaloneSolution(solutionName); - SetUpAnalyzerUpdate(embeddedAnalyzers, [], null); - - await testSubject.OnSolutionStateChangedAsync(null); - - await enterpriseRoslynAnalyzerProvider.DidNotReceiveWithAnyArgs().GetEnterpriseOrNullAsync(solutionName); - AssertRemoveFailedAndLogged(); - } - - [TestMethod] - public async Task CurrentConfigurationScopeChanged_SetsAnalyzers() - { - const string mySolution = "my solution"; - roslynWorkspaceWrapper.TryApplyChangesAsync(default).ReturnsForAnyArgs(Substitute.For()); - enterpriseRoslynAnalyzerProvider.GetEnterpriseOrNullAsync(mySolution).Returns(embeddedAnalyzers); - activeConfigScopeTracker.Current.Returns(new ConfigurationScope(mySolution)); - - activeConfigScopeTracker.CurrentConfigurationScopeChanged += Raise.EventWith(new(default)); - - await enterpriseRoslynAnalyzerProvider.Received(1).GetEnterpriseOrNullAsync(mySolution); - roslynWorkspaceWrapper.ReceivedWithAnyArgs(1).TryApplyChangesAsync(default); - AssertNoErrorsInLogs(); - } - - [TestMethod] - public async Task ActiveSolutionChanged_SetsAnalyzers() - { - const string solutionName = "my solution"; - roslynWorkspaceWrapper.TryApplyChangesAsync(default).ReturnsForAnyArgs(Substitute.For()); - enterpriseRoslynAnalyzerProvider.GetEnterpriseOrNullAsync(solutionName).Returns(embeddedAnalyzers); - - activeSolutionTracker.ActiveSolutionChanged += Raise.Event>(this, new ActiveSolutionChangedEventArgs(true, solutionName)); - - await enterpriseRoslynAnalyzerProvider.Received(1).GetEnterpriseOrNullAsync(solutionName); - roslynWorkspaceWrapper.ReceivedWithAnyArgs(1).TryApplyChangesAsync(default); - AssertNoErrorsInLogs(); - } - - private void SetUpAnalyzerUpdate( - IReadOnlyList analyzersToRemove, - IReadOnlyList analyzersToAdd, - IRoslynSolutionWrapper resultingSolution) - { - analyzersToRemove ??= []; - analyzersToAdd ??= []; - roslynWorkspaceWrapper.TryApplyChangesAsync( - Arg.Is( - x => - x.AnalyzersToRemove.SequenceEqual(analyzersToRemove, DefaultComparer) - && x.AnalyzersToAdd.SequenceEqual(analyzersToAdd, DefaultComparer))) - .Returns(resultingSolution); - } - - private void AssertNoErrorsInLogs() - { - logger.AssertPartialOutputStringDoesNotExist(Resources.RoslynAnalyzersNotUpdated); - logger.AssertPartialOutputStringDoesNotExist(Resources.RoslynAnalyzersNotRemoved); - } - private void AssertUpdateFailedAndLogged() => logger.AssertPartialOutputStringExists(Resources.RoslynAnalyzersNotUpdated); - private void AssertRemoveFailedAndLogged() => logger.AssertPartialOutputStringExists(Resources.RoslynAnalyzersNotRemoved); - - private void EnableDefaultEmbeddedAnalyzers() - { - basicRoslynAnalyzerProvider.GetBasicAsync().Returns(embeddedAnalyzers); - } - - private async Task SetUpStandaloneSolution(string solutionName) - { - EnableDefaultEmbeddedAnalyzers(); - await SimulateSolutionSet(Substitute.For(), solutionName); - } - - private async Task SimulateSolutionSet(IRoslynSolutionWrapper resultingSolution, string solutionName) - { - roslynWorkspaceWrapper.TryApplyChangesAsync(Arg.Any()).Returns(resultingSolution); - await testSubject.OnSolutionStateChangedAsync(solutionName); - ClearSubstitutes(); - } - - private void ClearSubstitutes() - { - basicRoslynAnalyzerProvider.ClearSubstitute(); - enterpriseRoslynAnalyzerProvider.ClearSubstitute(); - analyzerComparer.ClearSubstitute(); - roslynWorkspaceWrapper.ClearSubstitute(); - } -} diff --git a/src/Infrastructure.VS.UnitTests/SpanTranslatorTests.cs b/src/Infrastructure.VS.UnitTests/SpanTranslatorTests.cs new file mode 100644 index 0000000000..acd6f4d87f --- /dev/null +++ b/src/Infrastructure.VS.UnitTests/SpanTranslatorTests.cs @@ -0,0 +1,31 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.Infrastructure.VS.UnitTests; + +[TestClass] +public class SpanTranslatorTests +{ + [TestMethod] + public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported(); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); +} diff --git a/src/Infrastructure.VS.UnitTests/packages.lock.json b/src/Infrastructure.VS.UnitTests/packages.lock.json index f3573e697a..34e16ffc97 100644 --- a/src/Infrastructure.VS.UnitTests/packages.lock.json +++ b/src/Infrastructure.VS.UnitTests/packages.lock.json @@ -141,16 +141,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "Humanizer.Core": { "type": "Transitive", "resolved": "2.2.0", @@ -1527,8 +1517,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/Infrastructure.VS/Roslyn/EmbeddedDotnetAnalyzerProvider.cs b/src/Infrastructure.VS/Roslyn/EmbeddedDotnetAnalyzerProvider.cs deleted file mode 100644 index 69b0fc516d..0000000000 --- a/src/Infrastructure.VS/Roslyn/EmbeddedDotnetAnalyzerProvider.cs +++ /dev/null @@ -1,77 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Immutable; -using System.ComponentModel.Composition; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.ConfigurationScope; - -namespace SonarLint.VisualStudio.Infrastructure.VS.Roslyn; - -[Export(typeof(IBasicRoslynAnalyzerProvider))] -[Export(typeof(IEnterpriseRoslynAnalyzerProvider))] -[PartCreationPolicy(CreationPolicy.Shared)] -[method: ImportingConstructor] -public class EmbeddedDotnetAnalyzerProvider( - IEmbeddedDotnetAnalyzersLocator locator, - IAnalyzerAssemblyLoaderFactory analyzerAssemblyLoaderFactory, - IConfigurationScopeDotnetAnalyzerIndicator indicator, - ILogger logger, - IThreadHandling threadHandling) - : IBasicRoslynAnalyzerProvider, IEnterpriseRoslynAnalyzerProvider -{ - private readonly IAnalyzerAssemblyLoader loader = analyzerAssemblyLoaderFactory.Create(); - private ImmutableArray? basicAnalyzers; - private ImmutableArray? enterpriseAnalyzers; - - public Task> GetBasicAsync() => - threadHandling.RunOnBackgroundThread(() => - { - basicAnalyzers ??= CreateAnalyzerFileReferences(locator.GetBasicAnalyzerFullPaths()); - - return Task.FromResult(basicAnalyzers.Value); - }); - - public Task?> GetEnterpriseOrNullAsync(string configurationScopeId) => - threadHandling.RunOnBackgroundThread(async () => - { - if (!await indicator.ShouldUseEnterpriseCSharpAnalyzerAsync(configurationScopeId)) - { - return null; - } - - enterpriseAnalyzers ??= CreateAnalyzerFileReferences(locator.GetEnterpriseAnalyzerFullPaths()); - return enterpriseAnalyzers; - - }); - - private ImmutableArray CreateAnalyzerFileReferences(List analyzerPaths) - { - if (analyzerPaths.Count == 0) - { - logger.LogVerbose(Resources.EmbeddedRoslynAnalyzersNotFound); - throw new InvalidOperationException(Resources.EmbeddedRoslynAnalyzersNotFound); - } - - return analyzerPaths.Select(path => new AnalyzerFileReference(path, loader)).ToImmutableArray(); - } -} diff --git a/src/Infrastructure.VS/Roslyn/IRoslynSolutionWrapper.cs b/src/Infrastructure.VS/Roslyn/IRoslynSolutionWrapper.cs deleted file mode 100644 index d665c92cd2..0000000000 --- a/src/Infrastructure.VS/Roslyn/IRoslynSolutionWrapper.cs +++ /dev/null @@ -1,92 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; -using System.Text; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace SonarLint.VisualStudio.Infrastructure.VS.Roslyn; - -internal interface IRoslynSolutionWrapper -{ - IRoslynSolutionWrapper RemoveAnalyzerReferences(IReadOnlyCollection analyzers); - - IRoslynSolutionWrapper AddAnalyzerReferences(IReadOnlyCollection analyzers); - - Solution RoslynSolution { get; } - - string DisplayCurrentAnalyzerState(); - - bool ContainsAnalyzer(AnalyzerFileReference analyzer); -} - -[ExcludeFromCodeCoverage] -internal class RoslynSolutionWrapper(Solution solution) : IRoslynSolutionWrapper -{ - public bool ContainsAnalyzer(AnalyzerFileReference analyzer) => solution.AnalyzerReferences.Contains(analyzer); - - public IRoslynSolutionWrapper RemoveAnalyzerReferences(IReadOnlyCollection analyzers) => - new RoslynSolutionWrapper(analyzers.Aggregate(solution, (current, analyzer) => current.RemoveAnalyzerReference(analyzer))); - - public IRoslynSolutionWrapper AddAnalyzerReferences(IReadOnlyCollection analyzers) => - new RoslynSolutionWrapper(solution.AddAnalyzerReferences(analyzers)); - - public Solution RoslynSolution => solution; - - public string DisplayCurrentAnalyzerState() - { - try - { - var stringBuilder = new StringBuilder(); - - stringBuilder.AppendLine($"Solution {solution.FilePath}, {solution.Version} Analyzers"); - foreach (var currentSolutionAnalyzer in solution.AnalyzerReferences) - { - PrintAnalyzer(stringBuilder, currentSolutionAnalyzer); - } - - foreach (var projectId in solution.ProjectIds) - { - var project = solution.GetProject(projectId)!; - stringBuilder.AppendLine($"Project {project.Name} Analyzers"); - foreach (var projectAnalyzer in project.AnalyzerReferences) - { - PrintAnalyzer(stringBuilder, projectAnalyzer); - } - } - - return stringBuilder.ToString(); - } - catch - { - // this code is purely for debugging/investigating, so we don't care about exceptions here - return "Failed to list solution analyzers"; - } - } - - private static void PrintAnalyzer(StringBuilder stringBuilder, AnalyzerReference analyzer) => stringBuilder.AppendLine($" {analyzer.DisplayInfo()}"); -} - -internal static class AnalyzerReferenceExtensions -{ - public static string DisplayInfo(this AnalyzerReference analyzer) => $"{analyzer?.Display}, {analyzer?.Id}, {analyzer?.FullPath}"; -} diff --git a/src/Infrastructure.VS/Roslyn/IRoslynWorkspaceWrapper.cs b/src/Infrastructure.VS/Roslyn/IRoslynWorkspaceWrapper.cs deleted file mode 100644 index b740614d68..0000000000 --- a/src/Infrastructure.VS/Roslyn/IRoslynWorkspaceWrapper.cs +++ /dev/null @@ -1,103 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Immutable; -using System.ComponentModel.Composition; -using System.Diagnostics.CodeAnalysis; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.VisualStudio.LanguageServices; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.SystemAbstractions; - -namespace SonarLint.VisualStudio.Infrastructure.VS.Roslyn; - -internal interface IAnalyzerChange -{ - ImmutableArray AnalyzersToAdd { get; } - ImmutableArray AnalyzersToRemove { get; } - - IRoslynSolutionWrapper Change(IRoslynSolutionWrapper solution); -} - -internal class AnalyzerChange(ImmutableArray analyzersToRemove, ImmutableArray analyzersToAdd) : IAnalyzerChange -{ - public ImmutableArray AnalyzersToAdd { get; } = analyzersToAdd; - public ImmutableArray AnalyzersToRemove { get; } = analyzersToRemove; - - public IRoslynSolutionWrapper Change(IRoslynSolutionWrapper solution) - { - var analyzersToRemoveFiltered = AnalyzersToRemove.RemoveAll(x => !solution.ContainsAnalyzer(x)); - if (analyzersToRemoveFiltered.Any()) - { - solution = solution.RemoveAnalyzerReferences(analyzersToRemoveFiltered); - } - var analyzersToAddFiltered = AnalyzersToAdd.RemoveAll(x => solution.ContainsAnalyzer(x)); - if (analyzersToAddFiltered.Any()) - { - solution = solution.AddAnalyzerReferences(analyzersToAddFiltered); - } - return solution; - } -} - -internal interface IRoslynWorkspaceWrapper -{ - IRoslynSolutionWrapper CurrentSolution { get; } - - Task TryApplyChangesAsync(IAnalyzerChange analyzerChange); -} - -[Export(typeof(IRoslynWorkspaceWrapper))] -[PartCreationPolicy(CreationPolicy.Shared)] -[method: ImportingConstructor] -internal class RoslynWorkspaceWrapper([Import(typeof(VisualStudioWorkspace))] Workspace workspace, IThreadHandling threadHandling) : IRoslynWorkspaceWrapper -{ - private const int ApplyRetryCount = 5; - - public IRoslynSolutionWrapper CurrentSolution => - new RoslynSolutionWrapper(workspace.CurrentSolution); - - public async Task TryApplyChangesAsync(IAnalyzerChange analyzerChange) - { - for (var attempt = 0; attempt < ApplyRetryCount; attempt++) - { - if (await TryApplyChangesInternalAsync(analyzerChange) is {} result) - { - return result; - } - } - - return null; - } - - private async Task TryApplyChangesInternalAsync(IAnalyzerChange analyzerChange) - { - IRoslynSolutionWrapper wasApplied = null; - await threadHandling.RunOnUIThreadAsync(() => - { - var currentSolution = CurrentSolution; - var updatedSolution = analyzerChange.Change(currentSolution); - - wasApplied = updatedSolution == currentSolution || workspace.TryApplyChanges(updatedSolution.RoslynSolution) ? CurrentSolution : null; - }); - return wasApplied; - } -} diff --git a/src/Infrastructure.VS/Roslyn/SolutionRoslynAnalyzerManager.cs b/src/Infrastructure.VS/Roslyn/SolutionRoslynAnalyzerManager.cs deleted file mode 100644 index 61d353e824..0000000000 --- a/src/Infrastructure.VS/Roslyn/SolutionRoslynAnalyzerManager.cs +++ /dev/null @@ -1,193 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Immutable; -using System.ComponentModel.Composition; -using System.Text; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.VisualStudio.Threading; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.ConfigurationScope; -using SonarLint.VisualStudio.Core.Synchronization; - -namespace SonarLint.VisualStudio.Infrastructure.VS.Roslyn; - -public interface ISolutionRoslynAnalyzerManager : IDisposable -{ -} - -[Export(typeof(ISolutionRoslynAnalyzerManager))] -[PartCreationPolicy(CreationPolicy.Shared)] -internal sealed class SolutionRoslynAnalyzerManager : ISolutionRoslynAnalyzerManager -{ - private static readonly ImmutableArray NoChange = ImmutableArray.Empty; - private bool disposed; - private readonly IEqualityComparer?> analyzerComparer; - private readonly IActiveConfigScopeTracker activeConfigScopeTracker; - private readonly IActiveSolutionTracker activeSolutionTracker; - private readonly IAsyncLock asyncLock; - private readonly IEnterpriseRoslynAnalyzerProvider enterpriseAnalyzerProvider; - private readonly IBasicRoslynAnalyzerProvider basicAnalyzerProvider; - private readonly ILogger logger; - private readonly IRoslynWorkspaceWrapper roslynWorkspace; - private ImmutableArray? currentAnalyzers; - - [ImportingConstructor] - public SolutionRoslynAnalyzerManager( - IBasicRoslynAnalyzerProvider basicAnalyzerProvider, - IEnterpriseRoslynAnalyzerProvider enterpriseAnalyzerProvider, - IRoslynWorkspaceWrapper roslynWorkspace, - IActiveConfigScopeTracker activeConfigScopeTracker, - IActiveSolutionTracker activeSolutionTracker, - IAsyncLockFactory asyncLockFactory, - ILogger logger) - : this(basicAnalyzerProvider, - enterpriseAnalyzerProvider, - roslynWorkspace, - AnalyzerArrayComparer.Instance, - activeConfigScopeTracker, - activeSolutionTracker, - asyncLockFactory, - logger) - { - } - - internal /* for testing */ SolutionRoslynAnalyzerManager( - IBasicRoslynAnalyzerProvider basicAnalyzerProvider, - IEnterpriseRoslynAnalyzerProvider enterpriseAnalyzerProvider, - IRoslynWorkspaceWrapper roslynWorkspace, - IEqualityComparer?> analyzerComparer, - IActiveConfigScopeTracker activeConfigScopeTracker, - IActiveSolutionTracker activeSolutionTracker, - IAsyncLockFactory asyncLockFactory, - ILogger logger) - { - this.basicAnalyzerProvider = basicAnalyzerProvider; - this.enterpriseAnalyzerProvider = enterpriseAnalyzerProvider; - this.roslynWorkspace = roslynWorkspace; - this.analyzerComparer = analyzerComparer; - this.activeConfigScopeTracker = activeConfigScopeTracker; - this.activeSolutionTracker = activeSolutionTracker; - this.asyncLock = asyncLockFactory.Create(); - this.logger = logger.ForVerboseContext("Roslyn Analyzers"); - - activeConfigScopeTracker.CurrentConfigurationScopeChanged += OnConfigurationScopeChanged; - activeSolutionTracker.ActiveSolutionChanged += OnActiveSolutionChanged; - } - - internal /*for testing*/ async Task OnSolutionStateChangedAsync(string solutionName) - { - using (await asyncLock.AcquireAsync()) - { - ThrowIfDisposed(); - - if (solutionName is null) - { - await RemoveCurrentAnalyzersAsync(); - return; - } - try - { - await UpdateAnalyzersIfChangedAsync(await ChooseAnalyzersAsync(solutionName)); - } - catch (Exception e) - { - logger.WriteLine(e.ToString()); - } - } - } - - private void ThrowIfDisposed() - { - if (disposed) - { - throw new ObjectDisposedException(nameof(SolutionRoslynAnalyzerManager)); - } - } - - public void Dispose() - { - if (disposed) - { - return; - } - activeConfigScopeTracker.CurrentConfigurationScopeChanged -= OnConfigurationScopeChanged; - activeSolutionTracker.ActiveSolutionChanged -= OnActiveSolutionChanged; - disposed = true; - } - - private async Task> ChooseAnalyzersAsync(string configurationScopeId) => - await enterpriseAnalyzerProvider.GetEnterpriseOrNullAsync(configurationScopeId) ?? await basicAnalyzerProvider.GetBasicAsync(); - - private async Task UpdateAnalyzersIfChangedAsync(ImmutableArray analyzersToUse) - { - logger.LogVerbose(new MessageLevelContext { VerboseContext = ["To Update"] }, PrintAnalyzersChoice(analyzersToUse)); - if (!DidAnalyzerChoiceChange(analyzersToUse)) - { - logger.LogVerbose(new MessageLevelContext { VerboseContext = ["No Update"] }, "Nothing to update"); - return; - } - - logger.LogVerbose(new MessageLevelContext { VerboseContext = ["Before Update"] }, roslynWorkspace.CurrentSolution?.DisplayCurrentAnalyzerState()); - var updatedSolution = await UpdateAnalyzersAsync(analyzersToUse); - logger.LogVerbose(new MessageLevelContext { VerboseContext = ["After Update"] }, updatedSolution?.DisplayCurrentAnalyzerState()); - } - - private bool DidAnalyzerChoiceChange(ImmutableArray analyzersToUse) => !analyzerComparer.Equals(currentAnalyzers, analyzersToUse); - - private async Task RemoveCurrentAnalyzersAsync() - { - if (!currentAnalyzers.HasValue || await roslynWorkspace.TryApplyChangesAsync(new AnalyzerChange(currentAnalyzers.Value, NoChange)) is not null) - { - currentAnalyzers = null; - return; - } - - logger.LogVerbose(Resources.RoslynAnalyzersNotRemoved); - } - - private async Task UpdateAnalyzersAsync(ImmutableArray analyzersToUse) - { - if (await roslynWorkspace.TryApplyChangesAsync(new AnalyzerChange(currentAnalyzers ?? NoChange, analyzersToUse)) is { } solution) - { - currentAnalyzers = analyzersToUse; - return solution; - } - - logger.WriteLine(Resources.RoslynAnalyzersNotUpdated); - return null; - } - - private static string PrintAnalyzersChoice(ImmutableArray analyzersToUse) - { - var stringBuilder = new StringBuilder(); - stringBuilder.AppendLine("Analyzer update. Registering the following analyzers:"); - foreach (var analyzer in analyzersToUse) - { - stringBuilder.AppendLine($" {analyzer.DisplayInfo()}"); - } - var messageFormat = stringBuilder.ToString(); - return messageFormat; - } - - private void OnConfigurationScopeChanged(object sender, EventArgs e) => OnSolutionStateChangedAsync(activeConfigScopeTracker.Current?.Id).Forget(); - - private void OnActiveSolutionChanged(object sender, ActiveSolutionChangedEventArgs e) => OnSolutionStateChangedAsync(e?.SolutionName).Forget(); -} diff --git a/src/Infrastructure.VS/SpanTranslator.cs b/src/Infrastructure.VS/SpanTranslator.cs index 3305e663e7..7698724837 100644 --- a/src/Infrastructure.VS/SpanTranslator.cs +++ b/src/Infrastructure.VS/SpanTranslator.cs @@ -18,6 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; using Microsoft.VisualStudio.Text; namespace SonarLint.VisualStudio.Infrastructure.VS @@ -30,8 +32,11 @@ public interface ISpanTranslator SnapshotSpan TranslateTo(SnapshotSpan original, ITextSnapshot targetSnapshot, SpanTrackingMode spanTrackingMode); } + [Export(typeof(ISpanTranslator))] + [PartCreationPolicy(CreationPolicy.Shared)] public class SpanTranslator : ISpanTranslator { + [ExcludeFromCodeCoverage] public SnapshotSpan TranslateTo(SnapshotSpan original, ITextSnapshot targetSnapshot, SpanTrackingMode spanTrackingMode) => diff --git a/src/Infrastructure.VS/packages.lock.json b/src/Infrastructure.VS/packages.lock.json index 48e18829fa..b6474f7051 100644 --- a/src/Infrastructure.VS/packages.lock.json +++ b/src/Infrastructure.VS/packages.lock.json @@ -161,16 +161,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "Humanizer.Core": { "type": "Transitive", "resolved": "2.2.0", @@ -1337,16 +1327,6 @@ "System.IO.Abstractions": "[9.0.4, )", "System.Threading.Channels": "[7.0.0, )" } - }, - "sonarqube.client": { - "type": "Project", - "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", - "Newtonsoft.Json": "[13.0.3, )", - "SonarLint.VisualStudio.Core": "[1.0.0, )", - "System.Net.Http": "[4.0.0, )" - } } } } diff --git a/src/Integration.UnitTests/CSharpVB/Install/ImportsBeforeFileGeneratorTests.cs b/src/Integration.UnitTests/CSharpVB/Install/ImportsBeforeFileGeneratorTests.cs deleted file mode 100644 index 2d9434bbe1..0000000000 --- a/src/Integration.UnitTests/CSharpVB/Install/ImportsBeforeFileGeneratorTests.cs +++ /dev/null @@ -1,198 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.IO; -using System.Reflection; -using System.Xml; -using NSubstitute.ExceptionExtensions; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.SystemAbstractions; -using SonarLint.VisualStudio.Integration.CSharpVB.Install; -using SonarLint.VisualStudio.Integration.Resources; -using SonarLint.VisualStudio.TestInfrastructure; - -namespace SonarLint.VisualStudio.Integration.UnitTests.CSharpVB.Install; - -[TestClass] -public class ImportsBeforeFileGeneratorTests -{ - private static readonly string PathToDirectory = GetPathToImportBefore(); - private static readonly string PathToFile = Path.Combine(PathToDirectory, "SonarLint.targets"); - private const string ResourceContent = "some content"; - private IFileSystemService fileSystem; - private ILogger logger; - private ImportBeforeFileGenerator testSubject; - private IThreadHandling threadHandling; - private IEmbeddedResourceReader embeddedResourceReader; - - [TestInitialize] - public void TestInitialize() - { - logger = Substitute.For(); - fileSystem = Substitute.For(); - threadHandling = Substitute.For(); - embeddedResourceReader = Substitute.For(); - logger.ForContext(Arg.Any()).Returns(logger); - - testSubject = new ImportBeforeFileGenerator(logger, fileSystem, threadHandling, embeddedResourceReader); - } - - [TestMethod] - public void MefCtor_CheckExports() => - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - - [TestMethod] - public void Mef_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); - - [TestMethod] - public void Logger_LogContextIsSet() => logger.Received(1).ForContext(Strings.ImportsBeforeFileGeneratorLogContext); - - [TestMethod] - public async Task UpdateOrCreateTargetsFileAsync_RunsOnBackgroundThread() - { - await testSubject.UpdateOrCreateTargetsFileAsync(); - - threadHandling.ReceivedCalls().Should().ContainSingle(x => x.GetMethodInfo().Name == nameof(IThreadHandling.RunOnBackgroundThread)); - } - - [TestMethod] - public void FileDoesNotExist_CreatesFileWithWritesCorrectContent() - { - MockEmbeddedResourceReader(ResourceContent); - MockDirectoryExists(PathToDirectory, exists: true); - MockReadAllText(PathToFile, ResourceContent, exists: false); - - testSubject.UpdateOrCreateTargetsFile(); - - fileSystem.File.Received(1).WriteAllText(PathToFile, ResourceContent); - } - - [TestMethod] - public void FileExists_DifferentText_CreatesFile() - { - MockEmbeddedResourceReader(ResourceContent); - MockDirectoryExists(PathToDirectory, exists: true); - MockReadAllText(PathToFile, "wrong text"); - - testSubject.UpdateOrCreateTargetsFile(); - - fileSystem.File.Received(1).WriteAllText(PathToFile, ResourceContent); - } - - [TestMethod] - public void FileExists_SameText_DoesNotCreateFile() - { - MockEmbeddedResourceReader(ResourceContent); - MockDirectoryExists(PathToDirectory, exists: true); - MockReadAllText(PathToFile, ResourceContent); - - testSubject.UpdateOrCreateTargetsFile(); - - fileSystem.File.DidNotReceive().WriteAllText(PathToFile, Arg.Any()); - } - - [TestMethod] - public void FileExists_EmbeddedResourceCanNotBeRead_DoesNotUpdateFileAndLogs() - { - MockEmbeddedResourceReader(content: null); - MockDirectoryExists(PathToDirectory, exists: true); - MockReadAllText(PathToFile, ResourceContent); - - testSubject.UpdateOrCreateTargetsFile(); - - fileSystem.File.DidNotReceive().WriteAllText(PathToFile, Arg.Any()); - logger.Received(1).LogVerbose(Strings.ImportBeforeFileGenerator_ContentOfTargetsFileCanNotBeRead, "SonarLint.targets"); - } - - [TestMethod] - public void DirectoryDoesNotExist_CreatesDirectory() - { - MockEmbeddedResourceReader(ResourceContent); - MockDirectoryExists(PathToDirectory, exists: false); - MockFileExists(PathToFile, exists: false); - - testSubject.UpdateOrCreateTargetsFile(); - - fileSystem.Directory.Received(1).CreateDirectory(PathToDirectory); - } - - [TestMethod] - public void ThrowsNonCriticalException_Catches() - { - fileSystem.Directory.Exists(Arg.Any()).Throws(new NotImplementedException("this is a test")); - - testSubject.UpdateOrCreateTargetsFile(); - - logger.Received(1).WriteLine(Arg.Is(x => x.Contains(Strings.ImportBeforeFileGenerator_FailedToWriteFile)), "this is a test"); - } - - [TestMethod] - public void ThrowsCriticalException_ThrowsException() - { - fileSystem.Directory.Exists(Arg.Any()).Throws(new StackOverflowException()); - - var act = () => testSubject.UpdateOrCreateTargetsFile(); - - act.Should().Throw(); - } - - [TestMethod] - public void ConvertResourceToXml_DoesNotThrow() - { - var fileContent = GetTargetFileContent(); - - var xmlDoc = new XmlDocument(); - var act = () => xmlDoc.LoadXml(fileContent); - - act.Should().NotThrow(); - } - - private static string GetPathToImportBefore() - { - var localAppData = Environment.GetEnvironmentVariable("LOCALAPPDATA"); - var pathToImportsBefore = Path.Combine(localAppData, "Microsoft", "MSBuild", "Current", "Microsoft.Common.targets", "ImportBefore"); - - return pathToImportsBefore; - } - - private static string GetTargetFileContent() - { - var resourcePath = "SonarLint.VisualStudio.Integration.CSharpVB.Install.SonarLintTargets.xml"; - using var stream = new StreamReader(typeof(ImportBeforeFileGenerator).Assembly.GetManifestResourceStream(resourcePath)); - - return stream.ReadToEnd(); - } - - private void MockDirectoryExists(string path, bool exists) => fileSystem.Directory.Exists(path).Returns(exists); - - private void MockFileExists(string path, bool exists) => fileSystem.File.Exists(path).Returns(exists); - - private void MockReadAllText(string path, string content, bool exists = true) - { - MockFileExists(path, exists); - fileSystem.File.ReadAllText(path).Returns(content); - } - - private void MockEmbeddedResourceReader(string content) => embeddedResourceReader.Read(Arg.Any(), Arg.Any()).Returns(content); -} diff --git a/src/Integration.UnitTests/CSharpVB/RoslynConfigGeneratorTests.cs b/src/Integration.UnitTests/CSharpVB/RoslynConfigGeneratorTests.cs deleted file mode 100644 index ee13590fc5..0000000000 --- a/src/Integration.UnitTests/CSharpVB/RoslynConfigGeneratorTests.cs +++ /dev/null @@ -1,86 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.CSharpVB; -using SonarLint.VisualStudio.Core.SystemAbstractions; -using SonarLint.VisualStudio.Integration.CSharpVB; -using SonarLint.VisualStudio.TestInfrastructure; - -namespace SonarLint.VisualStudio.Integration.UnitTests.CSharpVB; - -[TestClass] -public class RoslynConfigGeneratorTests -{ - private RoslynConfigGenerator testSubject; - private IFileSystemService fileSystemService; - private IGlobalConfigGenerator globalConfigGenerator; - private ISonarLintConfigGenerator sonarLintConfigGenerator; - private ISonarLintConfigurationXmlSerializer sonarLintConfigurationXmlSerializer; - - private readonly Language language = Language.VBNET; - private const string BaseDirectory = @"C:\base\dir"; - private const string SlconfigDirectory = BaseDirectory + @"\VB"; - private const string SlconfigFilePath = SlconfigDirectory + @"\SonarLint.xml"; - private const string GlobalconfigFilePath = BaseDirectory + @"\sonarlint_vb.globalconfig"; - private readonly Dictionary properties = new() { { "a", "b" } }; - private readonly IFileExclusions fileExclusions = Substitute.For(); - private readonly List ruleStatuses = [Substitute.For()]; - private readonly List ruleParameters = [Substitute.For()]; - - [TestInitialize] - public void TestInitialize() - { - fileSystemService = Substitute.For(); - globalConfigGenerator = Substitute.For(); - sonarLintConfigGenerator = Substitute.For(); - sonarLintConfigurationXmlSerializer = Substitute.For(); - testSubject = new RoslynConfigGenerator(fileSystemService, globalConfigGenerator, sonarLintConfigGenerator, sonarLintConfigurationXmlSerializer); - } - - [TestMethod] - public void MefCtor_CheckIsExported() => - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - - [TestMethod] - public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); - - [TestMethod] - public void GenerateAndSaveConfiguration_CallsGeneratorsWithCorrectParametersAndSaves() - { - var sonarLintConfiguration = new SonarLintConfiguration(); - var slconfigContent = "slconfig content"; - var globalconfigContent = "globalconfig content"; - sonarLintConfigGenerator.Generate(ruleParameters, properties, fileExclusions, language).Returns(sonarLintConfiguration); - sonarLintConfigurationXmlSerializer.Serialize(sonarLintConfiguration).Returns(slconfigContent); - globalConfigGenerator.Generate(ruleStatuses).Returns(globalconfigContent); - - testSubject.GenerateAndSaveConfiguration(language, BaseDirectory, properties, fileExclusions, ruleStatuses, ruleParameters); - - fileSystemService.Directory.Received().CreateDirectory(BaseDirectory); - fileSystemService.Directory.Received().CreateDirectory(SlconfigDirectory); - fileSystemService.File.Received().WriteAllText(SlconfigFilePath, slconfigContent); - fileSystemService.File.Received().WriteAllText(GlobalconfigFilePath, globalconfigContent); - } -} diff --git a/src/Integration.UnitTests/CSharpVB/SonarLintConfigGeneratorTests.cs b/src/Integration.UnitTests/CSharpVB/SonarLintConfigGeneratorTests.cs deleted file mode 100644 index e95a56febb..0000000000 --- a/src/Integration.UnitTests/CSharpVB/SonarLintConfigGeneratorTests.cs +++ /dev/null @@ -1,274 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.CSharpVB; -using SonarLint.VisualStudio.Integration.CSharpVB; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.Integration.UnitTests.CSharpVB; - -[TestClass] -public class SonarLintConfigGeneratorTests -{ - private SonarLintConfigGenerator testSubject; - private static readonly IEnumerable EmptyRules = Array.Empty(); - private static readonly IDictionary EmptyProperties = new Dictionary(); - private static readonly Language ValidLanguage = Language.CSharp; - private static readonly IReadOnlyDictionary ValidParams = new Dictionary { { "any", "any value" } }; - - [TestInitialize] - public void TestInitialize() => testSubject = new SonarLintConfigGenerator(LanguageProvider.Instance); - - [TestMethod] - public void MefCtor_CheckIsExported() => - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport()); - - [TestMethod] - public void MefCtor_CheckIsSingleton() => - MefTestHelpers.CheckIsSingletonMefComponent(); - - [TestMethod] - public void Generate_NullArguments_Throws() - { - Action act = () => testSubject.Generate(null, EmptyProperties, new ServerExclusions(), ValidLanguage); - act.Should().ThrowExactly().And.ParamName.Should().Be("rules"); - - act = () => testSubject.Generate(EmptyRules, null, new ServerExclusions(), ValidLanguage); - act.Should().ThrowExactly().And.ParamName.Should().Be("sonarProperties"); - - act = () => testSubject.Generate(EmptyRules, EmptyProperties, null, ValidLanguage); - act.Should().ThrowExactly().And.ParamName.Should().Be("fileExclusions"); - - act = () => testSubject.Generate(EmptyRules, EmptyProperties, new ServerExclusions(), null); - act.Should().ThrowExactly().And.ParamName.Should().Be("language"); - } - - [TestMethod] - [DataRow("xxx")] - [DataRow("CS")] // should be case-sensitive - [DataRow("vb")] // VB language key is "vbnet" - public void Generate_UnrecognisedLanguage_Throws(string languageKey) - { - Action act = () => testSubject.Generate(EmptyRules, EmptyProperties, new ServerExclusions(), - new Language(languageKey, "languageX", languageKey, new PluginInfo("pluginKey", null), new RepoInfo("repoKey"), settingsFileName: ".any")); - act.Should().ThrowExactly().And.ParamName.Should().Be("language"); - } - - [TestMethod] - [DataRow("cs")] - [DataRow("vbnet")] - public void Generate_NoActiveRulesOrSettings_ValidLanguage_ReturnsValidConfig(string languageKey) - { - var actual = testSubject.Generate(EmptyRules, EmptyProperties, new ServerExclusions(), ToLanguage(languageKey)); - - actual.Should().NotBeNull(); - actual.Rules.Should().BeEmpty(); - actual.Settings.Should().BeEmpty(); - } - - [TestMethod] - public void Generate_ValidSettings_OnlyLanguageSpecificSettingsReturned() - { - // Arrange - var properties = new Dictionary - { - { "sonar.cs.property1", "valid setting 1" }, - { "sonar.cs.property2", "valid setting 2" }, - { "sonar.vbnet.property1", "wrong language - not returned" }, - { "sonar.CS.property2", "wrong case - not returned" }, - { "sonar.cs.", "incorrect prefix - not returned" }, - { "xxx.cs.property1", "key does not match - not returned" }, - { ".does.not.match", "not returned" } - }; - - // Act - var actual = testSubject.Generate(EmptyRules, properties, new ServerExclusions(), Language.CSharp); - - // Assert - actual.Settings.Should().BeEquivalentTo(new Dictionary { { "sonar.cs.property1", "valid setting 1" }, { "sonar.cs.property2", "valid setting 2" } }); - } - - [TestMethod] - public void Generate_ValidSettings_AreSorted() - { - // Arrange - var properties = new Dictionary { { "sonar.cs.property3", "aaa" }, { "sonar.cs.property1", "bbb" }, { "sonar.cs.property2", "ccc" }, }; - - // Act - var actual = testSubject.Generate(EmptyRules, properties, new ServerExclusions(), Language.CSharp); - - // Assert - actual.Settings[0].Key.Should().Be("sonar.cs.property1"); - actual.Settings[0].Value.Should().Be("bbb"); - - actual.Settings[1].Key.Should().Be("sonar.cs.property2"); - actual.Settings[1].Value.Should().Be("ccc"); - - actual.Settings[2].Key.Should().Be("sonar.cs.property3"); - actual.Settings[2].Value.Should().Be("aaa"); - } - - [TestMethod] - public void Generate_ValidSettings_SecuredSettingsAreNotReturned() - { - // Arrange - var properties = new Dictionary - { - { "sonar.cs.property1.secured", "secure - should not be returned" }, - { "sonar.cs.property2", "valid setting" }, - { "sonar.cs.property3.SECURED", "secure - should not be returned2" }, - }; - - // Act - var actual = testSubject.Generate(EmptyRules, properties, new ServerExclusions(), Language.CSharp); - - // Assert - actual.Settings.Should().BeEquivalentTo(new Dictionary { { "sonar.cs.property2", "valid setting" } }); - } - - [TestMethod] - [DataRow("cs", "csharpsquid")] - [DataRow("vbnet", "vbnet")] - public void Generate_ValidRules_OnlyRulesFromKnownRepositoryReturned(string knownLanguageKey, string knownRepoKey) - { - // Arrange - var rules = new List() - { - CreateRuleWithValidParams("valid1", knownRepoKey), - CreateRuleWithValidParams("unknown1", "unknown.repo.key"), - CreateRuleWithValidParams("valid2", knownRepoKey), - CreateRuleWithValidParams("invalid2", "another.unknown.repo.key"), - CreateRuleWithValidParams("valid3", knownRepoKey) - }; - - // Act - var actual = testSubject.Generate(rules, EmptyProperties, new ServerExclusions(), ToLanguage(knownLanguageKey)); - - // Assert - actual.Rules.Select(r => r.Key).Should().BeEquivalentTo(new string[] { "valid1", "valid2", "valid3" }); - } - - [TestMethod] - [DataRow("cs")] - [DataRow("vbnet")] - public void Generate_SonarSecurityRules_AreNotReturned(string languageKey) - { - // Arrange - var rules = new List() - { - CreateRuleWithValidParams("valid1", $"roslyn.sonaranalyzer.security.{languageKey}"), CreateRuleWithValidParams("valid2", $"roslyn.sonaranalyzer.security.{languageKey}") - }; - - // Act - var actual = testSubject.Generate(rules, EmptyProperties, new ServerExclusions(), ToLanguage(languageKey)); - - // Assert - actual.Rules.Should().BeEmpty(); - } - - [TestMethod] - public void Generate_ValidRules_OnlyRulesWithParametersReturned() - { - // Arrange - var rule1Params = new Dictionary { { "param1", "value1" }, { "param2", "value2" } }; - var rule3Params = new Dictionary { { "param3", "value4" } }; - - var rules = new List() { CreateRule("s111", "csharpsquid", rule1Params), CreateRule("s222", "csharpsquid" /* no params */), CreateRule("s333", "csharpsquid", rule3Params) }; - - // Act - var actual = testSubject.Generate(rules, EmptyProperties, new ServerExclusions(), Language.CSharp); - - // Assert - actual.Rules.Count.Should().Be(2); - - actual.Rules[0].Key.Should().Be("s111"); - actual.Rules[0].Parameters.Should().BeEquivalentTo(rule1Params); - actual.Rules[1].Key.Should().Be("s333"); - actual.Rules[1].Parameters.Should().BeEquivalentTo(rule3Params); - } - - [TestMethod] - public void Generate_ValidRules_AreSorted() - { - // Arrange - var rules = new List() - { - CreateRule("s222", "csharpsquid", - new Dictionary { { "any", "any" } }), - CreateRule("s111", "csharpsquid", - new Dictionary { { "CCC", "value 1" }, { "BBB", "value 2" }, { "AAA", "value 3" } }), - CreateRule("s333", "csharpsquid", - new Dictionary { { "any", "any" } }) - }; - - // Act - var actual = testSubject.Generate(rules, EmptyProperties, new ServerExclusions(), Language.CSharp); - - // Assert - actual.Rules.Count.Should().Be(3); - - actual.Rules[0].Key.Should().Be("s111"); - actual.Rules[1].Key.Should().Be("s222"); - actual.Rules[2].Key.Should().Be("s333"); - - actual.Rules[0].Parameters[0].Key.Should().Be("AAA"); - actual.Rules[0].Parameters[0].Value.Should().Be("value 3"); - - actual.Rules[0].Parameters[1].Key.Should().Be("BBB"); - actual.Rules[0].Parameters[1].Value.Should().Be("value 2"); - - actual.Rules[0].Parameters[2].Key.Should().Be("CCC"); - actual.Rules[0].Parameters[2].Value.Should().Be("value 1"); - } - - [TestMethod] - public void Generate_HasExclusions_ExclusionsIncludedInConfig() - { - var exclusions = new ServerExclusions( - exclusions: new[] { "**/path1", "**/*/path2" }, - globalExclusions: new[] { "**/path3" }, - inclusions: new[] { "**/path4" }); - - var actual = testSubject.Generate(EmptyRules, EmptyProperties, exclusions, Language.CSharp); - - actual.Settings.Count.Should().Be(3); - - actual.Settings[0].Should().BeEquivalentTo(new SonarLintKeyValuePair { Key = "sonar.exclusions", Value = "**/path1,**/*/path2" }); - actual.Settings[1].Should().BeEquivalentTo(new SonarLintKeyValuePair { Key = "sonar.global.exclusions", Value = "**/path3" }); - actual.Settings[2].Should().BeEquivalentTo(new SonarLintKeyValuePair { Key = "sonar.inclusions", Value = "**/path4" }); - } - - private static IRuleParameters CreateRuleWithValidParams(string ruleKey, string repoKey) => CreateRule(ruleKey, repoKey, ValidParams); - - private static IRuleParameters CreateRule(string ruleKey, string repoKey, IReadOnlyDictionary parameters = null) - { - var ruleParameters = Substitute.For(); - ruleParameters.Key.Returns(ruleKey); - ruleParameters.RepositoryKey.Returns(repoKey); - ruleParameters.Parameters.Returns(parameters); - - return ruleParameters; - } - - private static Language ToLanguage(string sqLanguageKey) => LanguageProvider.Instance.GetLanguageFromLanguageKey(sqLanguageKey); -} diff --git a/src/Integration.UnitTests/CSharpVB/StandaloneMode/StandaloneRoslynSettingsUpdaterTests.cs b/src/Integration.UnitTests/CSharpVB/StandaloneMode/StandaloneRoslynSettingsUpdaterTests.cs deleted file mode 100644 index b61da9d82b..0000000000 --- a/src/Integration.UnitTests/CSharpVB/StandaloneMode/StandaloneRoslynSettingsUpdaterTests.cs +++ /dev/null @@ -1,219 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Immutable; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.CSharpVB; -using SonarLint.VisualStudio.Core.UserRuleSettings; -using SonarLint.VisualStudio.Integration.CSharpVB.StandaloneMode; -using SonarLint.VisualStudio.TestInfrastructure; - -namespace SonarLint.VisualStudio.Integration.UnitTests.CSharpVB.StandaloneMode; - -[TestClass] -public class StandaloneRoslynSettingsUpdaterTests -{ - private IRoslynConfigGenerator roslynConfigGenerator; - private StandaloneRoslynSettingsUpdater testSubject; - private ILanguageProvider languageProvider; - private IThreadHandling threadHandling; - - [TestInitialize] - public void TestInitialize() - { - roslynConfigGenerator = Substitute.For(); - languageProvider = Substitute.For(); - threadHandling = Substitute.For(); - threadHandling.RunOnBackgroundThread(Arg.Any>>()) - .Returns(info => info.Arg>>()()); - - testSubject = new StandaloneRoslynSettingsUpdater( - roslynConfigGenerator, - languageProvider, - threadHandling); - } - - [TestMethod] - public void MefCtor_CheckIsExported() => - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - - [TestMethod] - public void MefCtor_CheckIsSingleton() => - MefTestHelpers.CheckIsSingletonMefComponent(); - - [TestMethod] - public void Update_CallsGeneratorWithCorrectLanguageAndDirectory() - { - IReadOnlyList fakeRoslynLanguages = [Language.VBNET, Language.TSql, Language.C]; - languageProvider.RoslynLanguages.Returns(fakeRoslynLanguages); - var userSettings = new UserSettings(new AnalysisSettings(), @"APPDATA\SonarLint for Visual Studio\.global"); - - testSubject.Update(userSettings); - - Received.InOrder(() => - { - foreach (var language in fakeRoslynLanguages) - { - roslynConfigGenerator.GenerateAndSaveConfiguration( - language, - @"APPDATA\SonarLint for Visual Studio\.global", - Arg.Is>(x => x.Count == 0), - Arg.Any(), - Arg.Is>(x => x.Count == 0), - Arg.Is>(x => x.Count == 0)); - } - }); - } - - [TestMethod] - public void Update_CallsGeneratorWithCorrectProperties() - { - IReadOnlyList fakeRoslynLanguages = [Language.VBNET, Language.TSql, Language.C]; - languageProvider.RoslynLanguages.Returns(fakeRoslynLanguages); - var properties = ImmutableDictionary.Create().SetItem("key", "value"); - var userSettings = new UserSettings(new AnalysisSettings(analysisProperties: properties), "any"); - - testSubject.Update(userSettings); - - Received.InOrder(() => - { - foreach (var language in fakeRoslynLanguages) - { - roslynConfigGenerator.GenerateAndSaveConfiguration( - language, - Arg.Any(), - properties, - Arg.Any(), - Arg.Is>(x => x.Count == 0), - Arg.Is>(x => x.Count == 0)); - } - }); - } - - [TestMethod] - public void Update_ConvertsExclusionsCorrectly() - { - IReadOnlyList fakeRoslynLanguages = [Language.VBNET, Language.TSql, Language.C]; - languageProvider.RoslynLanguages.Returns(fakeRoslynLanguages); - - testSubject.Update(new UserSettings(new AnalysisSettings([], ["one", "two"], []), "any")); - - Received.InOrder(() => - { - foreach (var language in fakeRoslynLanguages) - { - roslynConfigGenerator.GenerateAndSaveConfiguration( - language, - Arg.Any(), - Arg.Is>(x => x.Count == 0), - Arg.Is(x => x.ToDictionary()["sonar.exclusions"] == "**/one,**/two"), - Arg.Is>(x => x.Count == 0), - Arg.Is>(x => x.Count == 0)); - } - }); - } - - [TestMethod] - public void Update_ConvertsRulesCorrectly() - { - IReadOnlyList fakeRoslynLanguages = [Language.VBNET]; - languageProvider.RoslynLanguages.Returns(fakeRoslynLanguages); - var rules = new Dictionary() - { - { "vbnet:S1", new RuleConfig(RuleLevel.On, new Dictionary { { "1", "11" } }) }, - { "vbnet:S2", new RuleConfig(RuleLevel.Off, []) }, - { "vbnet:S3", new RuleConfig(RuleLevel.On, []) }, - { "vbnet:S4", new RuleConfig(RuleLevel.Off, new Dictionary { { "4", "44" } }) }, - }; - - testSubject.Update(new UserSettings(new AnalysisSettings(rules, [], []), "any")); - - roslynConfigGenerator - .Received() - .GenerateAndSaveConfiguration( - Language.VBNET, - Arg.Any(), - Arg.Is>(x => x.Count == 0), - Arg.Any(), - Arg.Is>(x => x.Count == 4), - Arg.Is>(x => x.Count == 4)); - var statuses = roslynConfigGenerator.ReceivedCalls().Single().GetArguments()[4] as List; - statuses.Should().BeEquivalentTo(new List - { - new(new SonarCompositeRuleId("vbnet", "S1"), true), - new(new SonarCompositeRuleId("vbnet", "S2"), false), - new(new SonarCompositeRuleId("vbnet", "S3"), true), - new(new SonarCompositeRuleId("vbnet", "S4"), false), - }); - var parameters = roslynConfigGenerator.ReceivedCalls().Single().GetArguments()[5] as List; - parameters.Should().BeEquivalentTo(new List - { - new(new SonarCompositeRuleId("vbnet", "S1"), new Dictionary { { "1", "11" } }), - new(new SonarCompositeRuleId("vbnet", "S2"), new Dictionary()), - new(new SonarCompositeRuleId("vbnet", "S3"), new Dictionary()), - new(new SonarCompositeRuleId("vbnet", "S4"), new Dictionary { { "4", "44" } }), - }); - } - - [TestMethod] - public void Update_GroupsRulesByLanguage() - { - IReadOnlyList fakeRoslynLanguages = [Language.VBNET, Language.CSharp]; - languageProvider.RoslynLanguages.Returns(fakeRoslynLanguages); - var rules = new Dictionary() - { - { "vbnet:S1", new RuleConfig(default) }, { "vbnet:S2", new RuleConfig(default) }, { "csharpsquid:S3", new RuleConfig(default) }, { "cpp:S4", new RuleConfig(default) }, - }; - - testSubject.Update(new UserSettings(new AnalysisSettings(rules, [], []), "any")); - - Received.InOrder(() => - { - roslynConfigGenerator - .GenerateAndSaveConfiguration( - Language.VBNET, - Arg.Any(), - Arg.Is>(x => x.Count == 0), - Arg.Any(), - Arg.Is>(x => x.Count == 2), - Arg.Is>(x => x.Count == 2)); - roslynConfigGenerator - .GenerateAndSaveConfiguration( - Language.CSharp, - Arg.Any(), - Arg.Is>(x => x.Count == 0), - Arg.Any(), - Arg.Is>(x => x.Count == 1), - Arg.Is>(x => x.Count == 1)); - }); - roslynConfigGenerator - .DidNotReceive() - .GenerateAndSaveConfiguration( - Language.Cpp, - Arg.Any(), - Arg.Any>(), - Arg.Any(), - Arg.Any>(), - Arg.Any>()); - } -} diff --git a/src/Integration.UnitTests/MefServices/ActiveSolutionBoundTrackerTests.cs b/src/Integration.UnitTests/MefServices/ActiveSolutionBoundTrackerTests.cs index c3a4bcf321..079de04fb5 100644 --- a/src/Integration.UnitTests/MefServices/ActiveSolutionBoundTrackerTests.cs +++ b/src/Integration.UnitTests/MefServices/ActiveSolutionBoundTrackerTests.cs @@ -45,7 +45,6 @@ public class ActiveSolutionBoundTrackerTests private TestLogger testLogger; private ISonarQubeService sonarQubeServiceMock; - private IBoundSolutionGitMonitor gitEventsMonitor; private IConfigScopeUpdater configScopeUpdater; private IInitializationProcessorFactory initializationProcessorFactory; private MockableInitializationProcessor createdInitializationProcessor; @@ -59,8 +58,7 @@ public void TestInitialize() { configProvider = Substitute.ForPartsOf(); activeSolutionTracker = Substitute.ForPartsOf(); - gitEventsMonitor = Substitute.For(); - initializationDependencies = [activeSolutionTracker, gitEventsMonitor]; + initializationDependencies = [activeSolutionTracker]; testLogger = new TestLogger(); vsMonitorMock = Substitute.For(); vsMonitorMock.GetCmdUIContextCookie(ref BoundSolutionUIContext.Guid, out Arg.Any()) @@ -86,7 +84,6 @@ public void MefCtor_CheckIsExported() => MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); @@ -109,9 +106,7 @@ public void ActiveSolutionBoundTracker_Initialisation_Unbound() threadHandling.RunOnUIThreadAsync(Arg.Any()); vsMonitorMock.GetCmdUIContextCookie(ref BoundSolutionUIContext.Guid, out Arg.Any()); _ = sonarQubeServiceMock.IsConnected; - gitEventsMonitor.Refresh(); activeSolutionTracker.ActiveSolutionChanged += Arg.Any>(); - gitEventsMonitor.HeadChanged += Arg.Any(); createdInitializationProcessor.InitializeAsync(); }); } @@ -134,9 +129,7 @@ public void ActiveSolutionBoundTracker_Initialisation_Bound() vsMonitorMock.GetCmdUIContextCookie(ref BoundSolutionUIContext.Guid, out Arg.Any()); _ = sonarQubeServiceMock.IsConnected; sonarQubeServiceMock.ConnectAsync(Arg.Any(), Arg.Any()); - gitEventsMonitor.Refresh(); activeSolutionTracker.ActiveSolutionChanged += Arg.Any>(); - gitEventsMonitor.HeadChanged += Arg.Any(); createdInitializationProcessor.InitializeAsync(); }); } @@ -151,8 +144,6 @@ public void DisposeBeforeInitialized_DisposeAndInitializeDoNothingWithEvents() activeSolutionTracker.DidNotReceive().ActiveSolutionChanged += Arg.Any>(); activeSolutionTracker.DidNotReceive().ActiveSolutionChanged -= Arg.Any>(); - gitEventsMonitor.DidNotReceive().HeadChanged += Arg.Any(); - gitEventsMonitor.DidNotReceive().HeadChanged -= Arg.Any(); } [TestMethod] @@ -162,7 +153,6 @@ public void Dispose_UnsubscribesFromEvents() testSubject.Dispose(); activeSolutionTracker.Received().ActiveSolutionChanged -= Arg.Any>(); - gitEventsMonitor.Received().HeadChanged -= Arg.Any(); } [TestMethod] @@ -207,7 +197,6 @@ public void ActiveSolutionBoundTracker_HandleBinding_EventsAreNotTriggeredBefore ConfigureSolutionBinding(boundSonarQubeProject); testSubject.HandleBindingChange(); - gitEventsMonitor.HeadChanged += Raise.Event(); // state is unmodified and events are not raised until initialized bindingChangedHandler.DidNotReceiveWithAnyArgs().Invoke(default, default); @@ -227,34 +216,6 @@ public void ActiveSolutionBoundTracker_HandleBinding_EventsAreNotTriggeredBefore testSubject.CurrentConfiguration.Mode.Should().Be(SonarLintMode.Standalone); } - [TestMethod] - public void ActiveSolutionBoundTracker_GitHeadChanged_EventsAreNotTriggeredBeforeInitializationIsComplete() - { - ConfigureService(false); - ConfigureSolutionBinding(boundSonarQubeProject); - using var testSubject = CreateUninitializedTestSubject(out var barrier); - var bindingUpdatedEventHandler = Substitute.For(); - testSubject.SolutionBindingUpdated += bindingUpdatedEventHandler; - - testSubject.HandleBindingChange(); - activeSolutionTracker.SimulateActiveSolutionChanged(false, null); - activeSolutionTracker.SimulateActiveSolutionChanged(true, "name"); - gitEventsMonitor.HeadChanged += Raise.Event(); - - // state is unmodified and events are not raised until initialized - bindingUpdatedEventHandler.DidNotReceiveWithAnyArgs().Invoke(default, default); - configProvider.DidNotReceiveWithAnyArgs().GetConfiguration(); - testSubject.CurrentConfiguration.Mode.Should().Be(SonarLintMode.Standalone); - - barrier.SetResult(1); - testSubject.InitializationProcessor.InitializeAsync().GetAwaiter().GetResult(); - - // switch to connected mode to test binding updated event - testSubject.CurrentConfiguration.Mode.Should().Be(SonarLintMode.Connected); - gitEventsMonitor.HeadChanged += Raise.Event(); - bindingUpdatedEventHandler.ReceivedWithAnyArgs().Invoke(default, default); - } - [TestMethod] public void ActiveSolutionBoundTracker_WhenConnectionCannotBeEstablished_ReportAsStandalone() { @@ -593,61 +554,6 @@ public void HandleBindingChange_WhenProjectChanged_RaisedChangedEvents() eventCounter.SolutionBindingChangedCount.Should().Be(1); } - [TestMethod] - public void GitRepoUpdated_SolutionBindingUpdatedEventsRaised() - { - // Arrange - ConfigureService(false); - ConfigureSolutionBinding(new BoundServerProject("solution", "projectKey", new ServerConnection.SonarQube(new Uri("http://foo")))); - using var testSubject = CreateAndInitializeTestSubject(); - var eventCounter = new EventCounter(testSubject); - - // Act - gitEventsMonitor.HeadChanged += Raise.Event(); - - // Assert - eventCounter.PreSolutionBindingUpdatedCount.Should().Be(1); - eventCounter.SolutionBindingUpdatedCount.Should().Be(1); - eventCounter.PreSolutionBindingChangedCount.Should().Be(0); - eventCounter.SolutionBindingChangedCount.Should().Be(0); - - eventCounter.RaisedEventNames.Should().HaveCount(2); - eventCounter.RaisedEventNames[0].Should().Be(nameof(testSubject.PreSolutionBindingUpdated)); - eventCounter.RaisedEventNames[1].Should().Be(nameof(testSubject.SolutionBindingUpdated)); - } - - [TestMethod] - public void GitRepoUpdated_UnBoundProject_SolutionBingingUpdatedNotInvoked() - { - // Arrange - using var testSubject = CreateAndInitializeTestSubject(); - var eventCounter = new EventCounter(testSubject); - - // Act - ConfigureSolutionBinding(null); - gitEventsMonitor.HeadChanged += Raise.Event(); - - // Assert - eventCounter.RaisedEventNames.Should().HaveCount(0); - } - - [TestMethod] - public void ActiveSolutionChanged_GitEventsMonitorRefreshInvoked() - { - ConfigureService(true); - ConfigureSolutionBinding(new BoundServerProject("solution", "projectKey", new ServerConnection.SonarQube(new Uri("http://foo")))); - - // Arrange - using var testSubject = CreateAndInitializeTestSubject(); - gitEventsMonitor.ClearReceivedCalls(); // invoked during initialization - - // Act - activeSolutionTracker.SimulateActiveSolutionChanged(true, "solution"); - - // Assert - gitEventsMonitor.Received(1).Refresh(); - } - private ActiveSolutionBoundTracker CreateUninitializedTestSubject(out TaskCompletionSource barrier) { var tcs = barrier = new(); @@ -656,7 +562,7 @@ private ActiveSolutionBoundTracker CreateUninitializedTestSubject(out TaskComple createdInitializationProcessor = processor; MockableInitializationProcessor.ConfigureWithWait(processor, tcs); }); - return new ActiveSolutionBoundTracker(serviceProvider, activeSolutionTracker, configScopeUpdater, testLogger, gitEventsMonitor, configProvider, sonarQubeServiceMock, + return new ActiveSolutionBoundTracker(serviceProvider, activeSolutionTracker, configScopeUpdater, testLogger, configProvider, sonarQubeServiceMock, initializationProcessorFactory); } @@ -664,7 +570,7 @@ private ActiveSolutionBoundTracker CreateAndInitializeTestSubject() { initializationProcessorFactory = MockableInitializationProcessor.CreateFactory(threadHandling, new TestLogger(), processor => createdInitializationProcessor = processor); - var testSubject = new ActiveSolutionBoundTracker(serviceProvider, activeSolutionTracker, configScopeUpdater, testLogger, gitEventsMonitor, configProvider, sonarQubeServiceMock, + var testSubject = new ActiveSolutionBoundTracker(serviceProvider, activeSolutionTracker, configScopeUpdater, testLogger, configProvider, sonarQubeServiceMock, initializationProcessorFactory); testSubject.InitializationProcessor.InitializeAsync().GetAwaiter().GetResult(); return testSubject; @@ -732,18 +638,6 @@ public EventCounter(IActiveSolutionBoundTracker tracker) SolutionBindingChangedCount++; raisedEventNames.Add(nameof(IActiveSolutionBoundTracker.SolutionBindingChanged)); }; - - tracker.PreSolutionBindingUpdated += (_, _) => - { - PreSolutionBindingUpdatedCount++; - raisedEventNames.Add(nameof(IActiveSolutionBoundTracker.PreSolutionBindingUpdated)); - }; - - tracker.SolutionBindingUpdated += (_, _) => - { - SolutionBindingUpdatedCount++; - raisedEventNames.Add(nameof(IActiveSolutionBoundTracker.SolutionBindingUpdated)); - }; } } } diff --git a/src/Integration.UnitTests/packages.lock.json b/src/Integration.UnitTests/packages.lock.json index 76b6b1dc02..fdb8191384 100644 --- a/src/Integration.UnitTests/packages.lock.json +++ b/src/Integration.UnitTests/packages.lock.json @@ -224,16 +224,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -1315,8 +1305,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/Integration.Vsix.UnitTests/Analysis/DocumentEventsHandlerTests.cs b/src/Integration.Vsix.UnitTests/Analysis/DocumentEventsHandlerTests.cs index e67ff3531d..2f9cee5c37 100644 --- a/src/Integration.Vsix.UnitTests/Analysis/DocumentEventsHandlerTests.cs +++ b/src/Integration.Vsix.UnitTests/Analysis/DocumentEventsHandlerTests.cs @@ -103,9 +103,9 @@ public void Ctor_InitializesInCorrectOrder() testSubject.InitializationProcessor.InitializeAsync(); activeConfigScopeTracker.CurrentConfigurationScopeChanged += Arg.Any>(); activeCompilationDatabaseTracker.DatabaseChanged += Arg.Any(); - documentTracker.DocumentOpened += Arg.Any>(); + documentTracker.DocumentOpened += Arg.Any>(); documentTracker.DocumentClosed += Arg.Any>(); - documentTracker.DocumentSaved += Arg.Any>(); + documentTracker.DocumentSaved += Arg.Any>(); documentTracker.OpenDocumentRenamed += Arg.Any>(); documentTracker.GetOpenDocuments(); // the remaining logic is tested in other tests slCoreServiceProvider.TryGetTransientService(out Arg.Any()); @@ -177,7 +177,7 @@ public void Ctor_SlCoreServiceNotAvailable_WithOpenCFamilyFiles_AddsToCompilatio public void DocumentOpened_CFamily_VcxProject_AddFileToCompilationDbAndNotifiesSlCore() { CreateAndInitializeTestSubject(); - var args = new DocumentOpenedEventArgs(CFamilyDocument, string.Empty); + var args = new DocumentEventArgs(CFamilyDocument, string.Empty); documentTracker.DocumentOpened += Raise.EventWith(documentTracker, args); @@ -191,7 +191,7 @@ public void DocumentOpened_CFamily_CMakeProject_DoesNotAddFileToVcxCompilationDb { CreateAndInitializeTestSubject(); MockCompilationDatabaseType(CompilationDatabaseType.CMake); - var args = new DocumentOpenedEventArgs(CFamilyDocument, "content"); + var args = new DocumentEventArgs(CFamilyDocument, "content"); documentTracker.DocumentOpened += Raise.EventWith(documentTracker, args); @@ -260,7 +260,7 @@ public void OpenDocumentRenamed_CFamily_CMakeProject_DoesNotRenameFileInCompilat public void DocumentOpened_NonCFamily_DoesNotAddFileToVcxCompilationDbButNotifiesSlCore() { CreateAndInitializeTestSubject(); - var args = new DocumentOpenedEventArgs(NonCFamilyDocument, string.Empty); + var args = new DocumentEventArgs(NonCFamilyDocument, string.Empty); documentTracker.DocumentOpened += Raise.EventWith(documentTracker, args); @@ -286,7 +286,7 @@ public void DocumentClosed_NonCFamily_DoesNotRemoveFileFromCompilationDbButNotif public void DocumentSaved_CFamily_VcxProject_AddFileToCompilationDb() { CreateAndInitializeTestSubject(); - var args = new DocumentSavedEventArgs(CFamilyDocument, string.Empty); + var args = new DocumentEventArgs(CFamilyDocument, string.Empty); documentTracker.DocumentSaved += Raise.EventWith(documentTracker, args); @@ -298,7 +298,7 @@ public void DocumentSaved_CFamily_CmakeProject_DoesNotAddFileToVcxCompilationDb( { CreateAndInitializeTestSubject(); MockCompilationDatabaseType(CompilationDatabaseType.CMake); - var args = new DocumentSavedEventArgs(CFamilyDocument, "content"); + var args = new DocumentEventArgs(CFamilyDocument, "content"); documentTracker.DocumentSaved += Raise.EventWith(documentTracker, args); @@ -323,7 +323,7 @@ public void OpenDocumentRenamed_NonCFamily_DoesNotRenameFileFromCompilationDbBut [TestMethod] public void DocumentSaved_NonCFamily_DoesNothing() { - var args = new DocumentSavedEventArgs(NonCFamilyDocument, string.Empty); + var args = new DocumentEventArgs(NonCFamilyDocument, string.Empty); documentTracker.DocumentSaved += Raise.EventWith(documentTracker, args); @@ -336,7 +336,7 @@ public void DocumentOpened_ExecutesOnBackgroundThread() { CreateAndInitializeTestSubject(); threadHandling.ClearReceivedCalls(); - var args = new DocumentOpenedEventArgs(CFamilyDocument, string.Empty); + var args = new DocumentEventArgs(CFamilyDocument, string.Empty); documentTracker.DocumentOpened += Raise.EventWith(documentTracker, args); @@ -391,7 +391,7 @@ public void DocumentSaved_ExecutesOnBackgroundThread() { CreateAndInitializeTestSubject(); threadHandling.ClearReceivedCalls(); - var args = new DocumentSavedEventArgs(CFamilyDocument, "using System;"); + var args = new DocumentEventArgs(CFamilyDocument, "using System;"); documentTracker.DocumentSaved += Raise.EventWith(documentTracker, args); @@ -407,7 +407,7 @@ public void DocumentOpened_SlCoreServiceNotAvailable_DoesNotNotifySlCoreAndLogs( { CreateAndInitializeTestSubject(); MockFileRpcService(service: null, succeeds: false); - var args = new DocumentOpenedEventArgs(CFamilyDocument, string.Empty); + var args = new DocumentEventArgs(CFamilyDocument, string.Empty); documentTracker.DocumentOpened += Raise.EventWith(documentTracker, args); @@ -450,7 +450,7 @@ public void DocumentOpened_CurrentConfigScopeIsNull_DoesNotNotifySlCoreAndLogs() { CreateAndInitializeTestSubject(); MockCurrentConfigScope(configurationScope: null); - var args = new DocumentOpenedEventArgs(CFamilyDocument, string.Empty); + var args = new DocumentEventArgs(CFamilyDocument, string.Empty); documentTracker.DocumentOpened += Raise.EventWith(documentTracker, args); @@ -501,12 +501,12 @@ public void Dispose_BeforeInitialization_DoesNotUnsubscribe() activeConfigScopeTracker.DidNotReceive().CurrentConfigurationScopeChanged -= Arg.Any>(); activeCompilationDatabaseTracker.DidNotReceive().DatabaseChanged += Arg.Any(); activeCompilationDatabaseTracker.DidNotReceive().DatabaseChanged -= Arg.Any(); - documentTracker.DidNotReceive().DocumentOpened += Arg.Any>(); - documentTracker.DidNotReceive().DocumentOpened -= Arg.Any>(); + documentTracker.DidNotReceive().DocumentOpened += Arg.Any>(); + documentTracker.DidNotReceive().DocumentOpened -= Arg.Any>(); documentTracker.DidNotReceive().DocumentClosed += Arg.Any>(); documentTracker.DidNotReceive().DocumentClosed -= Arg.Any>(); - documentTracker.DidNotReceive().DocumentSaved += Arg.Any>(); - documentTracker.DidNotReceive().DocumentSaved -= Arg.Any>(); + documentTracker.DidNotReceive().DocumentSaved += Arg.Any>(); + documentTracker.DidNotReceive().DocumentSaved -= Arg.Any>(); documentTracker.DidNotReceive().OpenDocumentRenamed += Arg.Any>(); documentTracker.DidNotReceive().OpenDocumentRenamed -= Arg.Any>(); } @@ -523,8 +523,8 @@ public void Dispose_AfterInitialization_Unsubscribes() activeConfigScopeTracker.Received(1).CurrentConfigurationScopeChanged -= Arg.Any>(); activeCompilationDatabaseTracker.Received(1).DatabaseChanged -= Arg.Any(); documentTracker.Received(1).DocumentClosed -= Arg.Any>(); - documentTracker.Received(1).DocumentOpened -= Arg.Any>(); - documentTracker.Received(1).DocumentSaved -= Arg.Any>(); + documentTracker.Received(1).DocumentOpened -= Arg.Any>(); + documentTracker.Received(1).DocumentSaved -= Arg.Any>(); documentTracker.Received(1).OpenDocumentRenamed -= Arg.Any>(); } @@ -581,7 +581,6 @@ public void ActiveConfigScopeTracker_CurrentConfigurationScopeChanged_SlCoreServ logger.Received(1).LogVerbose(SLCoreStrings.ServiceProviderNotInitialized); } - [TestMethod] public void ActiveCompilationDatabaseTracker_DatabaseChanged_UpdatesCompilationDatabase() { @@ -684,5 +683,6 @@ private void MockThreadHandling() => private void MockCurrentConfigScope(ConfigurationScope configurationScope) => activeConfigScopeTracker.Current.Returns(configurationScope); - private void MockCompilationDatabaseType(CompilationDatabaseType? type) => activeCompilationDatabaseTracker.CurrentDatabase.Returns(type is null ? null : new CompilationDatabaseInfo("any", type.Value)); + private void MockCompilationDatabaseType(CompilationDatabaseType? type) => + activeCompilationDatabaseTracker.CurrentDatabase.Returns(type is null ? null : new CompilationDatabaseInfo("any", type.Value)); } diff --git a/src/Integration.Vsix.UnitTests/Analysis/MuteIssueCommandTests.cs b/src/Integration.Vsix.UnitTests/Analysis/MuteIssueCommandTests.cs index deb4905f11..9b11a33571 100644 --- a/src/Integration.Vsix.UnitTests/Analysis/MuteIssueCommandTests.cs +++ b/src/Integration.Vsix.UnitTests/Analysis/MuteIssueCommandTests.cs @@ -213,7 +213,7 @@ public void Execute_WhenNoIssueSelected_DoesNothing() [TestMethod] public void Execute_WithRoslynIssue_MutesIssue() { - var roslynIssue = SetupRoslynIssue(out var sonarQubeIssue); + var roslynIssue = SetupRoslynIssue(); testSubjectMenuCommand.Invoke(); @@ -268,7 +268,7 @@ public void Execute_WithNonRoslynIssue_WhenMuteIssueException_CatchesAndShowsMes [TestMethod] public void Execute_WithRoslynIssue_WhenMuteIssueException_CatchesAndShowsMessageBox() { - SetupRoslynIssue(out _); + SetupRoslynIssue(); muteIssuesService.ResolveIssueWithDialogAsync(Arg.Any()).ThrowsAsync(new MuteIssueException("Error while muting")); var act = () => testSubjectMenuCommand.Invoke(); @@ -292,7 +292,7 @@ public void Execute_WithNonRoslynIssue_WhenMuteIssueCommentFailedException_Catch [TestMethod] public void Execute_WithRoslynIssue_WhenMuteIssueCommentFailedException_CatchesAndShowsMessageBox() { - SetupRoslynIssue(out _); + SetupRoslynIssue(); muteIssuesService.ResolveIssueWithDialogAsync(Arg.Any()).ThrowsAsync(new MuteIssueException.MuteIssueCommentFailedException()); var act = () => testSubjectMenuCommand.Invoke(); @@ -316,7 +316,7 @@ public void Execute_WithNonRoslynIssue_WhenMuteIssueCancelledException_Catches() [TestMethod] public void Execute_WithRoslynIssue_WhenMuteIssueCancelledException_Catches() { - SetupRoslynIssue(out _); + SetupRoslynIssue(); muteIssuesService.ResolveIssueWithDialogAsync(Arg.Any()).ThrowsAsync(new MuteIssueException.MuteIssueCancelledException()); var act = () => testSubjectMenuCommand.Invoke(); @@ -333,10 +333,9 @@ public void SupportedRepos_AllKnownLanguagesAreSupported() supportedRepos.Should().HaveCount(languageProvider.AllKnownLanguages.Count); } - private IFilterableRoslynIssue SetupRoslynIssue(out SonarQubeIssue sonarQubeIssue) + private IFilterableRoslynIssue SetupRoslynIssue() { var roslynIssue = Substitute.For(); - sonarQubeIssue = DummySonarQubeIssueFactory.CreateServerIssue(); errorListHelper.TryGetIssueFromSelectedRow(out _).Returns(x => { x[0] = roslynIssue; diff --git a/src/Integration.Vsix.UnitTests/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocatorTests.cs b/src/Integration.Vsix.UnitTests/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocatorTests.cs index 7db8c7a109..5359747443 100644 --- a/src/Integration.Vsix.UnitTests/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocatorTests.cs +++ b/src/Integration.Vsix.UnitTests/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocatorTests.cs @@ -19,10 +19,11 @@ */ using System.IO; -using System.IO.Abstractions; -using SonarLint.VisualStudio.Infrastructure.VS.Roslyn; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.SystemAbstractions; using SonarLint.VisualStudio.Integration.Vsix.EmbeddedAnalyzers; using SonarLint.VisualStudio.Integration.Vsix.Helpers; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; namespace SonarLint.VisualStudio.Integration.UnitTests.EmbeddedAnalyzers; @@ -31,113 +32,55 @@ public class EmbeddedDotnetAnalyzersLocatorTests { private const string PathInsideVsix = "C:\\somePath"; + private static readonly string CSharpRegularAnalyzer = GetAnalyzerFullPath(PathInsideVsix, "SonarAnalyzer.CSharp.dll"); + private static readonly string VbRegularAnalyzer = GetAnalyzerFullPath(PathInsideVsix, "SonarAnalyzer.VisualBasic.dll"); + private static readonly string CSharpEnterpriseAnalyzer = GetAnalyzerFullPath(PathInsideVsix, "SonarAnalyzer.Enterprise.CSharp.dll"); + private static readonly string VbEnterpriseAnalyzer = GetAnalyzerFullPath(PathInsideVsix, "SonarAnalyzer.Enterprise.VisualBasic.dll"); + private IFileSystemService fileSystem; + private EmbeddedDotnetAnalyzersLocator testSubject; private IVsixRootLocator vsixRootLocator; - private IFileSystem fileSystem; + private ILanguageProvider languageProvider; [TestInitialize] public void TestInitialize() { vsixRootLocator = Substitute.For(); - fileSystem = Substitute.For(); - testSubject = new EmbeddedDotnetAnalyzersLocator(vsixRootLocator, fileSystem); + fileSystem = Substitute.For(); + languageProvider = Substitute.For(); + testSubject = new EmbeddedDotnetAnalyzersLocator(vsixRootLocator, languageProvider, fileSystem); } [TestMethod] - public void MefCtor_CheckIsExported() - { + public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void MefCtor_IsSingleton() - { - MefTestHelpers.CheckIsSingletonMefComponent(); - } - - [TestMethod] - public void GetBasicAnalyzerFullPaths_AnalyzersExists_ReturnsFullPathsToAnalyzers() - { - string[] expectedPaths = - [ - GetAnalyzerFullPath(PathInsideVsix, "SonarAnalyzer.1.dll"), - GetAnalyzerFullPath(PathInsideVsix, "SonarAnalyzer.2.dll") - ]; - fileSystem.Directory.GetFiles(Arg.Any(), Arg.Any()).Returns(expectedPaths); - - var paths = testSubject.GetBasicAnalyzerFullPaths(); - - paths.Should().BeEquivalentTo(expectedPaths); - } + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); [TestMethod] - public void GetBasicAnalyzerFullPaths_EnterpriseAnalyzersExists_Skips() - { - string[] filePaths = - [ - GetAnalyzerFullPath(PathInsideVsix, "SonarAnalyzer.1.dll"), - GetAnalyzerFullPath(PathInsideVsix, "SonarAnalyzer.Enterprise.2.dll") - ]; - fileSystem.Directory.GetFiles(Arg.Any(), Arg.Any()).Returns(filePaths); - - var paths = testSubject.GetBasicAnalyzerFullPaths(); - - paths.Should().BeEquivalentTo(filePaths[0]); - } + public void MefCtor_IsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); [TestMethod] - public void GetBasicAnalyzerFullPaths_SearchesForFilesInsideVsix() + public void GetAnalyzerFullPathsByLanguage_ReturnsExpectedPaths() { - vsixRootLocator.GetVsixRoot().Returns(PathInsideVsix); - - testSubject.GetBasicAnalyzerFullPaths(); - - fileSystem.Directory.Received(1).GetFiles(Path.Combine(PathInsideVsix, "EmbeddedDotnetAnalyzerDLLs"), "SonarAnalyzer.*.dll"); + languageProvider.RoslynLanguages.Returns([Language.CSharp, Language.VBNET]); + fileSystem.Directory.GetFiles(Arg.Any(), Arg.Any()).Returns([ + CSharpRegularAnalyzer, + VbRegularAnalyzer, + CSharpEnterpriseAnalyzer, + VbEnterpriseAnalyzer + ]); + + testSubject.GetAnalyzerFullPathsByLicensedLanguage().Should().BeEquivalentTo( + new Dictionary> + { + [new LicensedRoslynLanguage(Language.CSharp, false)] = [CSharpRegularAnalyzer], + [new LicensedRoslynLanguage(Language.CSharp, true)] = [CSharpRegularAnalyzer, CSharpEnterpriseAnalyzer], + [new LicensedRoslynLanguage(Language.VBNET, false)] = [VbRegularAnalyzer], + [new LicensedRoslynLanguage(Language.VBNET, true)] = [VbRegularAnalyzer, VbEnterpriseAnalyzer], + }); } - [TestMethod] - public void GetEnterpriseAnalyzerFullPaths_AnalyzersExists_ReturnsFullPathsToAnalyzers() - { - string[] expectedPaths = - [ - GetAnalyzerFullPath(PathInsideVsix, "SonarAnalyzer.1.dll"), - GetAnalyzerFullPath(PathInsideVsix, "SonarAnalyzer.2.dll") - ]; - fileSystem.Directory.GetFiles(Arg.Any(), Arg.Any()).Returns(expectedPaths); - - var paths = testSubject.GetEnterpriseAnalyzerFullPaths(); - - paths.Should().BeEquivalentTo(expectedPaths); - } - - [TestMethod] - public void GetEnterpriseAnalyzerFullPaths_EnterpriseAnalyzersExists_ReturnsFullPathsToAnalyzers() - { - string[] expectedPaths = - [ - GetAnalyzerFullPath(PathInsideVsix, "SonarAnalyzer.1.dll"), - GetAnalyzerFullPath(PathInsideVsix, "SonarAnalyzer.Enterprise.2.dll") - ]; - fileSystem.Directory.GetFiles(Arg.Any(), Arg.Any()).Returns(expectedPaths); - - var paths = testSubject.GetEnterpriseAnalyzerFullPaths(); - - paths.Should().BeEquivalentTo(expectedPaths); - } - - [TestMethod] - public void GetAnalyzerFullPaths_SearchesForFilesInsideVsix() - { - vsixRootLocator.GetVsixRoot().Returns(PathInsideVsix); - - testSubject.GetEnterpriseAnalyzerFullPaths(); - - fileSystem.Directory.Received(1).GetFiles(Path.Combine(PathInsideVsix, "EmbeddedDotnetAnalyzerDLLs"), "SonarAnalyzer.*.dll"); - } - - private static string GetAnalyzerFullPath(string pathInsideVsix, string analyzerFile) - { - return Path.Combine(pathInsideVsix, analyzerFile); - } + private static string GetAnalyzerFullPath(string pathInsideVsix, string analyzerFile) => Path.Combine(pathInsideVsix, analyzerFile); } diff --git a/src/Integration.Vsix.UnitTests/Integration.Vsix.UnitTests.csproj b/src/Integration.Vsix.UnitTests/Integration.Vsix.UnitTests.csproj index 2da37e19f3..3f8ca9d72b 100644 --- a/src/Integration.Vsix.UnitTests/Integration.Vsix.UnitTests.csproj +++ b/src/Integration.Vsix.UnitTests/Integration.Vsix.UnitTests.csproj @@ -28,6 +28,7 @@ + diff --git a/src/Integration.Vsix.UnitTests/RoslynQuickFixes/RoslynQuickFixStorageTests.cs b/src/Integration.Vsix.UnitTests/RoslynQuickFixes/RoslynQuickFixStorageTests.cs new file mode 100644 index 0000000000..0855fca3c9 --- /dev/null +++ b/src/Integration.Vsix.UnitTests/RoslynQuickFixes/RoslynQuickFixStorageTests.cs @@ -0,0 +1,128 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.CodeAnalysis.CodeActions; +using SonarLint.VisualStudio.Core.ConfigurationScope; +using SonarLint.VisualStudio.Integration.Vsix.RoslynQuickFixes; +using SonarLint.VisualStudio.IssueVisualization.Models; +using SonarLint.VisualStudio.RoslynAnalyzerServer; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.Integration.UnitTests.RoslynQuickFixes; + +[TestClass] +public class RoslynQuickFixStorageTests +{ + private IActiveConfigScopeTracker configScopeTracker; + private RoslynQuickFixStorage testSubject; + + [TestInitialize] + public void TestInitialize() + { + configScopeTracker = Substitute.For(); + testSubject = new RoslynQuickFixStorage(configScopeTracker); + } + + [TestMethod] + public void MefCtor_CheckIsExported() + { + var requiredExports = new []{MefTestHelpers.CreateExport()}; + MefTestHelpers.CheckTypeCanBeImported(requiredExports); + MefTestHelpers.CheckTypeCanBeImported(requiredExports); + } + + [TestMethod] + public void MefCtor_CheckIsSingleton() => + MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public void Add_ThenTryGet_ReturnsTrueAndQuickFix() + { + var id = Guid.NewGuid(); + var quickFixImpl = CreateApplication(); + + testSubject.Add(id, quickFixImpl); + + var result = testSubject.TryGet(id, out var retrievedQuickFix); + + result.Should().BeTrue(); + retrievedQuickFix.Should().BeOfType().Which.Implementation.Should().BeSameAs(quickFixImpl); + } + + [TestMethod] + public void Add_ExistingId_OverwritesExistingValue() + { + var id = Guid.NewGuid(); + var originalQuickFixImpl = CreateApplication(); + var newQuickFixImpl = CreateApplication(); + + testSubject.Add(id, originalQuickFixImpl); + testSubject.Add(id, newQuickFixImpl); + + var result = testSubject.TryGet(id, out var retrievedQuickFix); + + result.Should().BeTrue(); + retrievedQuickFix.Should().BeOfType().Which.Implementation.Should().BeSameAs(newQuickFixImpl); + } + + [TestMethod] + public void TryGet_NonExistentId_ReturnsFalseAndNull() + { + var result = testSubject.TryGet(Guid.NewGuid(), out var retrievedQuickFix); + + result.Should().BeFalse(); + retrievedQuickFix.Should().BeNull(); + } + + [TestMethod] + public void ConfigScopeTracker_OnCurrentConfigurationScopeChanged_DefinitionChanged_ClearsCache() + { + var id = Guid.NewGuid(); + var quickFixImpl = CreateApplication(); + testSubject.Add(id, quickFixImpl); + + configScopeTracker.CurrentConfigurationScopeChanged += Raise.EventWith( + new ConfigurationScopeChangedEventArgs(definitionChanged: true)); + + var result = testSubject.TryGet(id, out var retrievedQuickFix); + + result.Should().BeFalse(); + retrievedQuickFix.Should().BeNull(); + } + + [TestMethod] + public void ConfigScopeTracker_OnCurrentConfigurationScopeChanged_DefinitionNotChanged_DoesNotClearCache() + { + var id = Guid.NewGuid(); + var quickFixImpl = CreateApplication(); + testSubject.Add(id, quickFixImpl); + + configScopeTracker.CurrentConfigurationScopeChanged += Raise.EventWith( + new ConfigurationScopeChangedEventArgs(definitionChanged: false)); + + var result = testSubject.TryGet(id, out var retrievedQuickFix); + + result.Should().BeTrue(); + retrievedQuickFix.Should().BeOfType().Which.Implementation.Should().BeSameAs(quickFixImpl); + } + + private static RoslynQuickFixApplicationImpl CreateApplication() => + new(Substitute.For(), Substitute.For(), Substitute.For()); +} diff --git a/src/Integration.Vsix.UnitTests/SLCore/SLCoreEmbeddedPluginJarLocatorTests.cs b/src/Integration.Vsix.UnitTests/SLCore/SLCoreEmbeddedPluginJarProviderTests.cs similarity index 87% rename from src/Integration.Vsix.UnitTests/SLCore/SLCoreEmbeddedPluginJarLocatorTests.cs rename to src/Integration.Vsix.UnitTests/SLCore/SLCoreEmbeddedPluginJarProviderTests.cs index 4a27e6e14a..7424f07da9 100644 --- a/src/Integration.Vsix.UnitTests/SLCore/SLCoreEmbeddedPluginJarLocatorTests.cs +++ b/src/Integration.Vsix.UnitTests/SLCore/SLCoreEmbeddedPluginJarProviderTests.cs @@ -29,13 +29,13 @@ namespace SonarLint.VisualStudio.Integration.Vsix.UnitTests.SLCore; [TestClass] -public class SLCoreEmbeddedPluginJarLocatorTests +public class SLCoreEmbeddedPluginJarProviderTests { private IDirectory directory; private IFileSystem fileSystem; private ILanguageProvider languageProvider; private ILogger logger; - private SLCoreEmbeddedPluginJarLocator testSubject; + private SLCoreEmbeddedPluginProvider testSubject; private IVsixRootLocator vsixRootLocator; [TestInitialize] @@ -46,7 +46,7 @@ public void TestInitialize() logger = Substitute.For(); directory = Substitute.For(); MockLanguageProvider(); - testSubject = new SLCoreEmbeddedPluginJarLocator(vsixRootLocator, fileSystem, logger, languageProvider); + testSubject = new SLCoreEmbeddedPluginProvider(vsixRootLocator, fileSystem, logger, languageProvider); MockVsixLocator(); MockFileSystem(); @@ -54,13 +54,13 @@ public void TestInitialize() [TestMethod] public void MefCtor_CheckIsExported() => - MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CheckTypeCanBeImported( MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(languageProvider)); [TestMethod] - public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); [TestMethod] public void ListJarFiles_DirectoryNotExists_ReturnsEmpty() @@ -157,19 +157,29 @@ public void ListConnectedModeEmbeddedPluginPathsByKey_NoJars_ReturnsEmptyDiction var result = testSubject.ListConnectedModeEmbeddedPluginPathsByKey(); result.Should().BeEmpty(); - logger.Received(4).LogVerbose(Strings.ConnectedModeEmbeddedPluginJarLocator_JarsNotFound); + logger.Received(testSubject.StandalonePlugins.Count).LogVerbose(Strings.ConnectedModeEmbeddedPluginJarLocator_JarsNotFound); } [TestMethod] - public void StandalonePlugins_ReturnsStandalonePluginsExceptRoslyn() + public void StandalonePlugins_ReturnsStandalonePluginsIncludingRoslyn() { _ = languageProvider.Received(1).LanguagesInStandaloneMode; - _ = languageProvider.Received(1).RoslynLanguages; + _ = languageProvider.DidNotReceive().RoslynLanguages; - var expectedPlugins = languageProvider.LanguagesInStandaloneMode.Except(languageProvider.RoslynLanguages).Select(x => x.PluginInfo).ToHashSet(); + var expectedPlugins = languageProvider.LanguagesInStandaloneMode.Select(x => x.PluginInfo).ToHashSet(); testSubject.StandalonePlugins.Should().BeEquivalentTo(expectedPlugins); } + [TestMethod] + public void ListDisabledPluginKeysForAnalysis_ReturnsCsharpAndVbNetPluginKeys() + { + List expectedPluginKeys = ["csharpenterprise", "vbnetenterprise"]; + + var result = testSubject.ListDisabledPluginKeysForAnalysis(); + + result.Should().BeEquivalentTo(expectedPluginKeys); + } + private void MockFileSystem(bool directoryExists, params string[] files) { directory.Exists(default).ReturnsForAnyArgs(false); @@ -198,8 +208,8 @@ private void MockLanguageProvider() languageProvider = Substitute.For(); // doesn't have to be the complete list for testing purposes languageProvider.LanguagesInStandaloneMode.Returns([ - Language.CSharp, Language.C, Language.Js, Language.Css, Language.Html, Language.Secrets + Language.CSharp, Language.VBNET, Language.C, Language.Js, Language.Css, Language.Html, Language.Secrets ]); - languageProvider.RoslynLanguages.Returns([Language.CSharp]); + languageProvider.RoslynLanguages.Returns([Language.CSharp, Language.VBNET,]); } } diff --git a/src/Integration.Vsix.UnitTests/SonarLintTagger/ResettableOneShotTimerTests.cs b/src/Integration.Vsix.UnitTests/SonarLintTagger/ResettableOneShotTimerTests.cs new file mode 100644 index 0000000000..a168ff228f --- /dev/null +++ b/src/Integration.Vsix.UnitTests/SonarLintTagger/ResettableOneShotTimerTests.cs @@ -0,0 +1,53 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Integration.Vsix.SonarLintTagger; + +namespace SonarLint.VisualStudio.Integration.UnitTests.SonarLintTagger; + +[TestClass] +public class ResettableOneShotTimerTests +{ + [TestMethod] + public async Task ResettableOneShotTimer_SmokeTest() + { + var timerTimeSpan = TimeSpan.FromMilliseconds(100); + var testSubject = new ResettableOneShotTimer(timerTimeSpan); + var eventHandler = Substitute.For(); + testSubject.Elapsed += eventHandler; + + testSubject.Reset(); + await Task.Delay(2 * (int)timerTimeSpan.TotalMilliseconds); + + eventHandler.ReceivedWithAnyArgs(1).Invoke(default, default); + } + + [TestMethod] + public void ResettableOneShotTimer_Dispose_DisposesTimer() + { + var timerTimeSpan = TimeSpan.FromMilliseconds(50); + var testSubject = new ResettableOneShotTimer(timerTimeSpan); + + testSubject.Dispose(); + + var act = () => testSubject.Reset(); + act.Should().Throw(); + } +} diff --git a/src/Integration.Vsix.UnitTests/SonarLintTagger/TaggerProviderTests.cs b/src/Integration.Vsix.UnitTests/SonarLintTagger/TaggerProviderTests.cs index b27493a452..2c6141ff4e 100644 --- a/src/Integration.Vsix.UnitTests/SonarLintTagger/TaggerProviderTests.cs +++ b/src/Integration.Vsix.UnitTests/SonarLintTagger/TaggerProviderTests.cs @@ -31,6 +31,7 @@ using SonarLint.VisualStudio.Integration.Vsix; using SonarLint.VisualStudio.Integration.Vsix.Analysis; using SonarLint.VisualStudio.Integration.Vsix.ErrorList; +using SonarLint.VisualStudio.Integration.Vsix.SonarLintTagger; using SonarLint.VisualStudio.IssueVisualization.Editor; using SonarLint.VisualStudio.IssueVisualization.Editor.LanguageDetection; @@ -57,6 +58,8 @@ public class TaggerProviderTests private IAnalyzer analyzer; private IInitializationProcessorFactory initializationProcessorFactory; private IThreadHandling threadHandling; + private ITaskExecutorWithDebounceFactory taskExecutorWithDebounceFactory; + private ITaskExecutorWithDebounce reanalysisExecutorWithDebounce; private static readonly AnalysisLanguage[] DetectedLanguagesJsTs = [AnalysisLanguage.TypeScript, AnalysisLanguage.Javascript]; @@ -90,6 +93,11 @@ public void SetUp() threadHandling = Substitute.ForPartsOf(); + taskExecutorWithDebounceFactory = Substitute.For(); + reanalysisExecutorWithDebounce = Substitute.For(); + taskExecutorWithDebounceFactory.Create(Arg.Any()).Returns(reanalysisExecutorWithDebounce); + reanalysisExecutorWithDebounce.When(x => x.Debounce(Arg.Any())).Do(call => call.Arg().Invoke()); + testSubject = CreateAndInitializeTestSubject(); } @@ -121,7 +129,8 @@ private static Export[] GetRequiredExports() => MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport() + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), ]; #endregion MEF tests @@ -141,12 +150,14 @@ public void Ctor_InitializesInCorrectOrder() => [TestMethod] public void CreateTagger_should_create_tracker_when_tagger_is_created() { + taskExecutorWithDebounceFactory.ClearReceivedCalls(); var doc = CreateMockedDocument("anyname"); var tagger = CreateTaggerForDocument(doc); tagger.Should().NotBeNull(); VerifyCreateIssueConsumerWasCalled(doc); + taskExecutorWithDebounceFactory.Received(1).Create(debounceTimeSpan: TimeSpan.FromMilliseconds(500)); } [TestMethod] @@ -341,7 +352,7 @@ public void FilterIssueTrackersByPath_WithPaths_AllMatched_AllTrackersReturned() [TestMethod] public void AddIssueTracker_RaisesEvent() { - var eventHandler = Substitute.For>(); + var eventHandler = Substitute.For>(); var filePath = "file1.txt"; var content = "some text"; testSubject.DocumentOpened += eventHandler; @@ -349,7 +360,7 @@ public void AddIssueTracker_RaisesEvent() testSubject.AddIssueTracker(issueTracker); - eventHandler.Received(1).Invoke(testSubject, Arg.Is(e => + eventHandler.Received(1).Invoke(testSubject, Arg.Is(e => e.Document.FullPath == filePath && e.Document.DetectedLanguages == DetectedLanguagesJsTs && e.Content == content)); @@ -384,7 +395,7 @@ public void IssueTracker_DocumentClosed_RaiseEvent() [TestMethod] public void IssueTracker_DocumentSaved_RaiseEvent() { - var eventHandler = Substitute.For>(); + var eventHandler = Substitute.For>(); var fileName = "anyname.js"; var doc = CreateMockedDocument(fileName, DetectedLanguagesJsTs); testSubject.DocumentSaved += eventHandler; @@ -392,7 +403,7 @@ public void IssueTracker_DocumentSaved_RaiseEvent() CreateTaggerForDocument(doc); RaiseFileEvent(doc, FileActionTypes.ContentSavedToDisk); - eventHandler.Received(1).Invoke(Arg.Any(), Arg.Is(x => x.Document.FullPath == fileName && x.Document.DetectedLanguages == DetectedLanguagesJsTs)); + eventHandler.Received(1).Invoke(Arg.Any(), Arg.Is(x => x.Document.FullPath == fileName && x.Document.DetectedLanguages == DetectedLanguagesJsTs)); } [TestMethod] @@ -425,6 +436,37 @@ public void IssueTracker_DocumentRename_RaiseEvent() Arg.Is(x => x.Document.FullPath == newName && x.OldFilePath == oldName && x.Document.DetectedLanguages == DetectedLanguagesJsTs)); } + [TestMethod] + public void IssueTracker_DocumentUpdated_RaiseEvent() + { + var eventHandler = Substitute.For>(); + var fileName = "anyname.js"; + string content = "new content"; + var doc = CreateMockedDocument(fileName, DetectedLanguagesJsTs); + testSubject.DocumentUpdated += eventHandler; + + CreateTaggerForDocument(doc); + testSubject.OnDocumentUpdated(fileName, content, DetectedLanguagesJsTs); + + eventHandler.Received(1).Invoke(Arg.Any(), Arg.Is(x => x.Document.FullPath == fileName + && x.Document.DetectedLanguages == DetectedLanguagesJsTs + && x.Content == content)); + } + + [TestMethod] + public void IssueTracker_DocumentUpdated_AddsNewFileToFileTracker() + { + var filePath = "anyname.js"; + string content = "new content"; + var doc = CreateMockedDocument(filePath, DetectedLanguagesJsTs, content: content); + CreateTaggerForDocument(doc); + mockFileTracker.ClearReceivedCalls(); + + testSubject.OnDocumentUpdated(filePath, content, DetectedLanguagesJsTs); + + mockFileTracker.Received(1).AddFiles(new SourceFile(filePath, encoding: null, content)); + } + [TestMethod] public void GetOpenDocuments_ReturnsAmountOfIssueTrackers() { @@ -464,6 +506,7 @@ public void AnalysisRequested_CallsAnalyzerRequestAnalysis() mockAnalysisRequester.AnalysisRequested += Raise.EventWith(this, new AnalysisRequestEventArgs(filesToaAnalyze)); analysisExecutingSignal.WaitOne(AnalysisTimeout); + reanalysisExecutorWithDebounce.Received(1).Debounce(Arg.Any()); analyzer.Received(1).ExecuteAnalysis(Arg.Is>(x => x.SequenceEqual(filesToaAnalyze))); } @@ -580,7 +623,7 @@ private TaggerProvider CreateAndInitializeTestSubject() var taggerProvider = new TaggerProvider( mockSonarErrorDataSource, dummyDocumentFactoryService, serviceProvider, mockSonarLanguageRecognizer, mockAnalysisRequester, vsProjectInfoProvider, issueConsumerFactory, issueConsumerStorage, - mockTaggableBufferIndicator, mockFileTracker, analyzer, logger, initializationProcessorFactory); + mockTaggableBufferIndicator, mockFileTracker, analyzer, logger, initializationProcessorFactory, taskExecutorWithDebounceFactory); taggerProvider.InitializationProcessor.InitializeAsync().GetAwaiter().GetResult(); return taggerProvider; } diff --git a/src/Integration.Vsix.UnitTests/SonarLintTagger/TaskExecutorWithDebounceFactoryTest.cs b/src/Integration.Vsix.UnitTests/SonarLintTagger/TaskExecutorWithDebounceFactoryTest.cs new file mode 100644 index 0000000000..da94c5fd1b --- /dev/null +++ b/src/Integration.Vsix.UnitTests/SonarLintTagger/TaskExecutorWithDebounceFactoryTest.cs @@ -0,0 +1,51 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Synchronization; +using SonarLint.VisualStudio.Integration.Vsix.SonarLintTagger; + +namespace SonarLint.VisualStudio.Integration.UnitTests.SonarLintTagger; + +[TestClass] +public class TaskExecutorWithDebounceFactoryTest +{ + private TaskExecutorWithDebounceFactory testSubject; + + [TestInitialize] + public void TestInitialize() => testSubject = new TaskExecutorWithDebounceFactory(Substitute.For()); + + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public void Create_ShouldReturnInstance() + { + var result = testSubject.Create(TimeSpan.FromMilliseconds(1)); + + result.Should().NotBeNull(); + result.Should().BeOfType(); + } +} diff --git a/src/Integration.Vsix.UnitTests/SonarLintTagger/TaskExecutorWithDebounceTest.cs b/src/Integration.Vsix.UnitTests/SonarLintTagger/TaskExecutorWithDebounceTest.cs new file mode 100644 index 0000000000..7bd93b53e0 --- /dev/null +++ b/src/Integration.Vsix.UnitTests/SonarLintTagger/TaskExecutorWithDebounceTest.cs @@ -0,0 +1,113 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Integration.Vsix.SonarLintTagger; + +namespace SonarLint.VisualStudio.Integration.UnitTests.SonarLintTagger; + +[TestClass] +public class TaskExecutorWithDebounceTest +{ + private TaskExecutorWithDebounce testSubject; + private NoOpThreadHandler threadHandling; + private IResettableOneShotTimer timer; + + [TestInitialize] + public void TestInitialize() + { + threadHandling = Substitute.ForPartsOf(); + timer = Substitute.For(); + testSubject = new TaskExecutorWithDebounce(timer, threadHandling); + } + + [TestMethod] + public void Debounce_TimerNotRaised_DoesNotExecuteAction() + { + var action = Substitute.For(); + + testSubject.Debounce(action); + + action.DidNotReceive().Invoke(); + timer.Received().Reset(); + } + + [TestMethod] + public void Debounce_NoActionSet_DoesNotThrow() + { + var act = () => timer.Elapsed += Raise.Event(); + + act.Should().NotThrow(); + } + + [TestMethod] + public void Debounce_ExecutesTaskWithDebounce() + { + var action = Substitute.For(); + testSubject.Debounce(action); + + timer.Elapsed += Raise.Event(); + + Received.InOrder(() => + { + timer.Reset(); + threadHandling.RunOnBackgroundThread(Arg.Any>>()); + action.Invoke(); + }); + } + + [TestMethod] + public void Debounce_MultipleTimes_UpdatesWithLatestState() + { + var action1 = Substitute.For(); + var action2 = Substitute.For(); + var action3 = Substitute.For(); + testSubject.Debounce(action1); + testSubject.Debounce(action2); + testSubject.Debounce(action3); + + timer.Elapsed += Raise.Event(); + + action1.DidNotReceive().Invoke(); + action2.DidNotReceive().Invoke(); + action3.Received().Invoke(); + } + + [TestMethod] + public void Debounce_MultipleTriggers_ActionOnlyExecutedOnce() + { + var action = Substitute.For(); + testSubject.Debounce(action); + + timer.Elapsed += Raise.Event(); + timer.Elapsed += Raise.Event(); + timer.Elapsed += Raise.Event(); + + action.Received(1).Invoke(); + } + + [TestMethod] + public void Dispose_DisposesTimer() + { + testSubject.Dispose(); + + timer.Received(1).Dispose(); + timer.Received(1).Elapsed -= Arg.Any(); + } +} diff --git a/src/Integration.Vsix.UnitTests/SonarLintTagger/TextBufferIssueTrackerTests.cs b/src/Integration.Vsix.UnitTests/SonarLintTagger/TextBufferIssueTrackerTests.cs index f12f7bb93b..3c85faf7b3 100644 --- a/src/Integration.Vsix.UnitTests/SonarLintTagger/TextBufferIssueTrackerTests.cs +++ b/src/Integration.Vsix.UnitTests/SonarLintTagger/TextBufferIssueTrackerTests.cs @@ -26,7 +26,6 @@ using Microsoft.VisualStudio.Utilities; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Analysis; -using SonarLint.VisualStudio.Core.Initialization; using SonarLint.VisualStudio.Core.Synchronization; using SonarLint.VisualStudio.Infrastructure.VS.Initialization; using SonarLint.VisualStudio.Integration.Vsix; @@ -57,6 +56,8 @@ public class TextBufferIssueTrackerTests private IIssueConsumerFactory issueConsumerFactory; private IIssueConsumerStorage issueConsumerStorage; private IIssueConsumer issueConsumer; + private ITaskExecutorWithDebounceFactory taskExecutorWithDebounceFactory; + private ITaskExecutorWithDebounce taskExecutorWithDebounce; [TestInitialize] public void SetUp() @@ -67,11 +68,15 @@ public void SetUp() issueConsumerFactory = Substitute.For(); issueConsumerStorage = Substitute.For(); issueConsumer = Substitute.For(); + taskExecutorWithDebounceFactory = Substitute.For(); + taskExecutorWithDebounce = Substitute.For(); taggerProvider = CreateTaggerProvider(); mockTextSnapshot = CreateTextSnapshotMock(); mockDocumentTextBuffer = CreateTextBufferMock(mockTextSnapshot); mockedJavascriptDocumentFooJs = CreateDocumentMock("foo.js", mockDocumentTextBuffer); javascriptLanguage = [AnalysisLanguage.Javascript]; + taskExecutorWithDebounceFactory.Create(Arg.Any()).Returns(taskExecutorWithDebounce); + MockIssueConsumerFactory(mockedJavascriptDocumentFooJs, issueConsumer); testSubject = CreateTestSubject(mockedJavascriptDocumentFooJs); @@ -99,6 +104,7 @@ public void Ctor_RegistersEventsTrackerAndFactory() taggerProvider.ActiveTrackersForTesting.Should().BeEquivalentTo(testSubject); mockedJavascriptDocumentFooJs.Received(1).FileActionOccurred += Arg.Any>(); + ((ITextBuffer2)mockDocumentTextBuffer).Received(1).ChangedOnBackground += Arg.Any>(); // Note: the test subject isn't responsible for adding the entry to the buffer.Properties // - that's done by the TaggerProvider. @@ -151,6 +157,9 @@ public void Dispose_CleansUpEventsAndRegistrations() taggerProvider.ActiveTrackersForTesting.Should().BeEmpty(); mockedJavascriptDocumentFooJs.Received(1).FileActionOccurred -= Arg.Any>(); + ((ITextBuffer2)mockDocumentTextBuffer).Received(1).ChangedOnBackground -= Arg.Any>(); + + taskExecutorWithDebounce.Received(1).Dispose(); } [TestMethod] @@ -167,13 +176,13 @@ public void Dispose_RaisesEvent() [TestMethod] public void WhenFileIsSaved_DocumentSavedEventIsRaised() { - var eventHandler = Substitute.For>(); + var eventHandler = Substitute.For>(); taggerProvider.DocumentSaved += eventHandler; RaiseFileSavedEvent(mockedJavascriptDocumentFooJs); eventHandler.Received(1).Invoke(taggerProvider, - Arg.Is(x => x.Document.FullPath == mockedJavascriptDocumentFooJs.FilePath && x.Document.DetectedLanguages == javascriptLanguage)); + Arg.Is(x => x.Document.FullPath == mockedJavascriptDocumentFooJs.FilePath && x.Document.DetectedLanguages == javascriptLanguage)); } [TestMethod] @@ -195,9 +204,8 @@ public void WhenFileIsSaved_AnalysisSnapshotIsUpdated() public void WhenFileIsLoaded_EventsAreNotRaised() { var renamedEventHandler = Substitute.For>(); - var savedEventHandler = Substitute.For>(); + var savedEventHandler = Substitute.For>(); taggerProvider.OpenDocumentRenamed += renamedEventHandler; - taggerProvider.DocumentSaved += savedEventHandler; RaiseFileLoadedEvent(mockedJavascriptDocumentFooJs); @@ -294,17 +302,6 @@ public void UpdateAnalysisState_NoProjectInformation_CreatesIssueConsumerCorrect VerifyCreateIssueConsumerWasCalled(textDocument, (default, Guid.Empty), consumer, new AnalysisSnapshot(textDocument.FilePath, textDocument.TextBuffer.CurrentSnapshot)); } - [TestMethod] - public void UpdateAnalysisState_ClearsErrorList() - { - var textDocument = mockedJavascriptDocumentFooJs; - - CreateTestSubject(textDocument).UpdateAnalysisState(); - - issueConsumer.Received().SetIssues(textDocument.FilePath, []); - issueConsumer.Received().SetHotspots(textDocument.FilePath, []); - } - [TestMethod] public void UpdateAnalysisState_NonCriticalException_IsSuppressed() { @@ -327,6 +324,21 @@ public void UpdateAnalysisState_CriticalException_IsNotSuppressed() .WithMessage("this is a test"); } + [TestMethod] + public void OnTextBufferChangedOnBackground_UpdatesAnalysisState() + { + var eventHandler = SubscribeToDocumentSaved(); + var newContent = "new content"; + var newSnapshot = CreateTextSnapshotMock(newContent); + var newAnalysisSnapshot = new AnalysisSnapshot(mockedJavascriptDocumentFooJs.FilePath, newSnapshot); + MockTaskExecutorWithDebounce(); + CreateTestSubject(mockedJavascriptDocumentFooJs); + + RaiseTextBufferChangedOnBackground(currentTextBuffer: mockDocumentTextBuffer, newSnapshot); + + VerifyAnalysisStateUpdated(mockedJavascriptDocumentFooJs, newAnalysisSnapshot, eventHandler, newContent); + } + private void SetUpIssueConsumerStorageThrows(Exception exception) => issueConsumerStorage.When(x => x.Remove(Arg.Any())).Do(x => throw exception); private static void VerifySingletonManagerDoesNotExist(ITextBuffer buffer) => FindSingletonManagerInPropertyCollection(buffer).Should().BeNull(); @@ -373,21 +385,21 @@ private TaggerProvider CreateTaggerProvider() var analysisRequester = mockAnalysisRequester; var provider = new TaggerProvider(sonarErrorListDataSource, textDocumentFactoryService, serviceProvider, languageRecognizer, analysisRequester, vsProjectInfoProvider, issueConsumerFactory, issueConsumerStorage, Mock.Of(), - mockFileTracker, analyzer, logger, new InitializationProcessorFactory(Substitute.For(), new NoOpThreadHandler(), new TestLogger())); + mockFileTracker, analyzer, logger, new InitializationProcessorFactory(Substitute.For(), new NoOpThreadHandler(), new TestLogger()), taskExecutorWithDebounceFactory); return provider; } - private static ITextSnapshot CreateTextSnapshotMock() + private static ITextSnapshot CreateTextSnapshotMock(string content = TextContent) { var textSnapshot = Substitute.For(); - textSnapshot.GetText().Returns(TextContent); + textSnapshot.GetText().Returns(content); return textSnapshot; } private static ITextBuffer CreateTextBufferMock(ITextSnapshot textSnapshot) { // Text buffer with a properties collection and current snapshot - var mockTextBuffer = Substitute.For(); + var mockTextBuffer = Substitute.For(); var dummyProperties = new PropertyCollection(); mockTextBuffer.Properties.Returns(dummyProperties); @@ -447,7 +459,7 @@ private TextBufferIssueTracker CreateTestSubject(ITextDocument textDocument) private TextBufferIssueTracker CreateTestSubject(ITextDocument textDocument, ILogger logger) => new(taggerProvider, textDocument, javascriptLanguage, - mockSonarErrorDataSource, vsProjectInfoProvider, issueConsumerFactory, issueConsumerStorage, logger); + mockSonarErrorDataSource, vsProjectInfoProvider, issueConsumerFactory, issueConsumerStorage, taskExecutorWithDebounce, logger); private void ClearIssueConsumerCalls() { @@ -455,4 +467,43 @@ private void ClearIssueConsumerCalls() issueConsumerFactory.ClearReceivedCalls(); vsProjectInfoProvider.ClearReceivedCalls(); } + + private static void RaiseTextBufferChangedOnBackground(ITextBuffer currentTextBuffer, ITextSnapshot newTextSnapshot) + { + var args = new TextContentChangedEventArgs(Substitute.For(), newTextSnapshot, EditOptions.DefaultMinimalChange, null); + ((ITextBuffer2)currentTextBuffer).ChangedOnBackground += Raise.EventWith(null, args); + } + + private EventHandler SubscribeToDocumentSaved() + { + var eventHandler = Substitute.For>(); + taggerProvider.DocumentUpdated += eventHandler; + return eventHandler; + } + + private void MockTaskExecutorWithDebounce() => + taskExecutorWithDebounce.When(x => x.Debounce(Arg.Any())).Do(callInfo => + { + var action = callInfo.Arg(); + action(); + }); + + private void VerifyAnalysisStateUpdated( + ITextDocument textDocument, + AnalysisSnapshot newAnalysisSnapshot, + EventHandler eventHandler, + string newContent) + { + issueConsumerStorage.Received().Remove(textDocument.FilePath); + vsProjectInfoProvider.Received().GetDocumentProjectInfo(newAnalysisSnapshot.FilePath); + issueConsumerFactory.Received().Create(textDocument, newAnalysisSnapshot.FilePath, newAnalysisSnapshot.TextSnapshot, Arg.Any(), Arg.Any(), + Arg.Any()); + issueConsumerStorage.Received().Set(textDocument.FilePath, Arg.Any()); + issueConsumer.DidNotReceiveWithAnyArgs().SetIssues(default, default); + issueConsumer.DidNotReceiveWithAnyArgs().SetHotspots(default, default); + eventHandler.Received().Invoke(taggerProvider, + Arg.Is(x => x.Document.FullPath == textDocument.FilePath + && x.Document.DetectedLanguages == javascriptLanguage + && x.Content == newContent)); + } } diff --git a/src/Integration.Vsix.UnitTests/packages.lock.json b/src/Integration.Vsix.UnitTests/packages.lock.json index a8135ba20b..afe6a8db8b 100644 --- a/src/Integration.Vsix.UnitTests/packages.lock.json +++ b/src/Integration.Vsix.UnitTests/packages.lock.json @@ -14,6 +14,19 @@ "resolved": "0.11.4", "contentHash": "zSCkwOgc5OyfMfEeMr9x0K7WCDf8i6VdF2RtCLN/4m6iebTtJQdeoJ9IS4/RyYHuLUYjrm0sd+siWbaSvSzRYQ==" }, + "Microsoft.CodeAnalysis.Workspaces.Common": { + "type": "Direct", + "requested": "[3.11.0, )", + "resolved": "3.11.0", + "contentHash": "YAbH4LCJfh8DhDGwYzSHqvnF06lKkVwblr8C+GwIYCv0i3Rzqjnbversat+i2n9k8twQ43yxVGTYK5p/mIOj4w==", + "dependencies": { + "Humanizer.Core": "2.2.0", + "Microsoft.Bcl.AsyncInterfaces": "5.0.0", + "Microsoft.CodeAnalysis.Common": "[3.11.0]", + "System.Composition": "1.0.31", + "System.IO.Pipelines": "5.0.1" + } + }, "Microsoft.CSharp": { "type": "Direct", "requested": "[4.7.0, )", @@ -149,15 +162,10 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { + "Humanizer.Core": { "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" + "resolved": "2.2.0", + "contentHash": "rsYXB7+iUPP8AHgQ8JP2UZI2xK2KhjcdGr9E6zX3CsZaTLCaw8M35vaAJRo1rfxeaZEVMuXeaquLVCkZ7JcZ5Q==" }, "MessagePack": { "type": "Transitive", @@ -200,6 +208,25 @@ "resolved": "16.5.0", "contentHash": "K0hfdWy+0p8DJXxzpNc4T5zHm4hf9QONAvyzvw3utKExmxRBShtV/+uHVYTblZWk+rIHNEHeglyXMmqfSshdFA==" }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.3.2", + "contentHash": "7xt6zTlIEizUgEsYAIgm37EbdkiMmr6fP6J9pDoKEpiGM4pi32BCPGr/IczmSJI9Zzp0a6HOzpr9OvpMP+2veA==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "3.11.0", + "contentHash": "FDKSkRRXnaEWMa2ONkLMo0ZAt/uiV1XIXyodwKIgP1AMIKA7JJKXx/OwFVsvkkUT4BeobLwokoxFw70fICahNg==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.3.2", + "System.Collections.Immutable": "5.0.0", + "System.Memory": "4.5.4", + "System.Reflection.Metadata": "5.0.0", + "System.Runtime.CompilerServices.Unsafe": "5.0.0", + "System.Text.Encoding.CodePages": "4.5.1", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, "Microsoft.CodeCoverage": { "type": "Transitive", "resolved": "16.6.1", @@ -1002,6 +1029,54 @@ "resolved": "4.5.0", "contentHash": "+iB9FoZnfdqMEGq6np28X6YNSUrse16CakmIhV3h6PxEWt7jYxUN3Txs1D8MZhhf4QmyvK0F/EcIN0f4gGN0dA==" }, + "System.Composition": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "I+D26qpYdoklyAVUdqwUBrEIckMNjAYnuPJy/h9dsQItpQwVREkDFs4b4tkBza0kT2Yk48Lcfsv2QQ9hWsh9Iw==", + "dependencies": { + "System.Composition.AttributedModel": "1.0.31", + "System.Composition.Convention": "1.0.31", + "System.Composition.Hosting": "1.0.31", + "System.Composition.Runtime": "1.0.31", + "System.Composition.TypedParts": "1.0.31" + } + }, + "System.Composition.AttributedModel": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "NHWhkM3ZkspmA0XJEsKdtTt1ViDYuojgSND3yHhTzwxepiwqZf+BCWuvCbjUt4fe0NxxQhUDGJ5km6sLjo9qnQ==" + }, + "System.Composition.Convention": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "GLjh2Ju71k6C0qxMMtl4efHa68NmWeIUYh4fkUI8xbjQrEBvFmRwMDFcylT8/PR9SQbeeL48IkFxU/+gd0nYEQ==", + "dependencies": { + "System.Composition.AttributedModel": "1.0.31" + } + }, + "System.Composition.Hosting": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "fN1bT4RX4vUqjbgoyuJFVUizAl2mYF5VAb+bVIxIYZSSc0BdnX+yGAxcavxJuDDCQ1K+/mdpgyEFc8e9ikjvrg==", + "dependencies": { + "System.Composition.Runtime": "1.0.31" + } + }, + "System.Composition.Runtime": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "0LEJN+2NVM89CE4SekDrrk5tHV5LeATltkp+9WNYrR+Huiyt0vaCqHbbHtVAjPyeLWIc8dOz/3kthRBj32wGQg==" + }, + "System.Composition.TypedParts": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "0Zae/FtzeFgDBBuILeIbC/T9HMYbW4olAmi8XqqAGosSOWvXfiQLfARZEhiGd0LVXaYgXr0NhxiU1LldRP1fpQ==", + "dependencies": { + "System.Composition.AttributedModel": "1.0.31", + "System.Composition.Hosting": "1.0.31", + "System.Composition.Runtime": "1.0.31" + } + }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", "resolved": "5.0.1", @@ -1081,6 +1156,14 @@ "resolved": "4.6.0", "contentHash": "j/V5HVvxvBQ7uubYD0PptQW2KGsi1Pc2kZ9yfwLixv3ADdjL/4M78KyC5e+ymW612DY8ZE4PFoZmWpoNmN2mqg==" }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "5NecZgXktdGg34rh1OenY1rFNDCI8xSjFr+Z4OU4cU06AQHUdRnIIEeWENu3Wl4YowbzkymAIMvi3WyK9U53pQ==", + "dependencies": { + "System.Collections.Immutable": "5.0.0" + } + }, "System.Runtime": { "type": "Transitive", "resolved": "4.3.0", @@ -1134,6 +1217,14 @@ "resolved": "5.0.0", "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "4J2JQXbftjPMppIHJ7IC+VXQ9XfEagN92vZZNoG12i+zReYlim5dMoXFC1Zzg7tsnKDM7JPo5bYfFK4Jheq44w==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "4.5.2" + } + }, "System.Threading.AccessControl": { "type": "Transitive", "resolved": "5.0.0", @@ -1265,7 +1356,7 @@ "SonarLint.VisualStudio.Integration": "[1.0.0, )", "SonarLint.VisualStudio.IssueVisualization": "[1.0.0, )", "SonarLint.VisualStudio.IssueVisualization.Security": "[1.0.0, )", - "SonarLint.VisualStudio.Roslyn.Suppressions": "[1.0.0, )", + "SonarLint.VisualStudio.RoslynAnalyzerServer": "[1.0.0, )", "SonarLint.VisualStudio.SLCore": "[1.0.0, )", "SonarLint.VisualStudio.SLCore.Listeners": "[1.0.0, )", "SonarQube.Client": "[1.0.0, )", @@ -1381,16 +1472,10 @@ "SonarLint.VisualStudio.IssueVisualization": "[1.0.0, )" } }, - "SonarLint.VisualStudio.Roslyn.Suppressions": { + "SonarLint.VisualStudio.RoslynAnalyzerServer": { "type": "Project", "dependencies": { - "Microsoft.VisualStudio.Sdk": "[17.0.31902.203, )", - "Newtonsoft.Json": "[13.0.3, )", - "SonarLint.VisualStudio.ConnectedMode": "[1.0.0, )", - "SonarLint.VisualStudio.Core": "[1.0.0, )", - "SonarLint.VisualStudio.Infrastructure.VS": "[1.0.0, )", - "SonarQube.Client": "[1.0.0, )", - "System.IO.Abstractions": "[9.0.4, )" + "SonarLint.VisualStudio.Core": "[1.0.0, )" } }, "SonarLint.VisualStudio.SLCore": { @@ -1405,14 +1490,13 @@ "dependencies": { "SonarLint.VisualStudio.ConnectedMode": "[1.0.0, )", "SonarLint.VisualStudio.IssueVisualization.Security": "[1.0.0, )", + "SonarLint.VisualStudio.RoslynAnalyzerServer": "[1.0.0, )", "SonarLint.VisualStudio.SLCore": "[1.0.0, )" } }, "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/Integration.Vsix/Analysis/DocumentEventsHandler.cs b/src/Integration.Vsix/Analysis/DocumentEventsHandler.cs index 35e4951c64..9308f8def0 100644 --- a/src/Integration.Vsix/Analysis/DocumentEventsHandler.cs +++ b/src/Integration.Vsix/Analysis/DocumentEventsHandler.cs @@ -151,7 +151,7 @@ private void OnDocumentClosed(object sender, DocumentEventArgs args) => NotifySlCoreFileClosed(args.Document.FullPath, activeConfigScopeTracker.Current); }).Forget(); - private void OnDocumentOpened(object sender, DocumentOpenedEventArgs args) => + private void OnDocumentOpened(object sender, DocumentEventArgs args) => threadHandling.RunOnBackgroundThread(async () => { await AddFilesToCompilationDatabaseAsync(args.Document); @@ -163,7 +163,7 @@ private void OnDocumentOpened(object sender, DocumentOpenedEventArgs args) => /// Due to the fact that we can't react to project/file properties changes, regenerating the compilation database entry on file save is needed /// Additionally, this is a workaround that can deal with the renaming bug described in https://sonarsource.atlassian.net/browse/SLVS-2170 /// - private void OnDocumentSaved(object sender, DocumentSavedEventArgs args) => + private void OnDocumentSaved(object sender, DocumentEventArgs args) => threadHandling.RunOnBackgroundThread(async () => { await AddFilesToCompilationDatabaseAsync(args.Document); diff --git a/src/Integration.Vsix/Analysis/IssueConsumerFactory.cs b/src/Integration.Vsix/Analysis/IssueConsumerFactory.cs index 65437c118e..848997e1ab 100644 --- a/src/Integration.Vsix/Analysis/IssueConsumerFactory.cs +++ b/src/Integration.Vsix/Analysis/IssueConsumerFactory.cs @@ -20,7 +20,6 @@ using System.ComponentModel.Composition; using Microsoft.VisualStudio.Text; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; using SonarLint.VisualStudio.Core.Analysis; using SonarLint.VisualStudio.IssueVisualization.Models; using SonarLint.VisualStudio.IssueVisualization.Security.Hotspots; @@ -42,7 +41,8 @@ internal interface IIssueConsumerFactory /// Instancing: a new issue consumer should be created for each analysis request /// i.e. the lifetime of the issue consumer should be tied to that analysis. /// - IIssueConsumer Create(ITextDocument textDocument, + IIssueConsumer Create( + ITextDocument textDocument, string analysisFilePath, ITextSnapshot analysisSnapshot, string projectName, @@ -64,7 +64,8 @@ internal IssueConsumerFactory(IAnalysisIssueVisualizationConverter converter, IL this.localHotspotsStore = localHotspotsStore; } - public IIssueConsumer Create(ITextDocument textDocument, + public IIssueConsumer Create( + ITextDocument textDocument, string analysisFilePath, ITextSnapshot analysisSnapshot, string projectName, diff --git a/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithStrongNames.txt b/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithStrongNames.txt index 3f3669778e..78a92e13b7 100644 --- a/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithStrongNames.txt +++ b/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithStrongNames.txt @@ -1,7 +1,7 @@ --- ################################ # Assembly references report -# Report date/time: 2025-10-14T13:19:29.6557597Z +# Report date/time: 2025-10-14T14:24:04.3472038Z ################################ # # Generated by Devtility CheckAsmRefs v0.11.0.223 @@ -51,7 +51,7 @@ Referenced assemblies: - 'SonarLint.VisualStudio.Integration, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'SonarLint.VisualStudio.IssueVisualization, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'SonarLint.VisualStudio.IssueVisualization.Security, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' -- 'SonarLint.VisualStudio.Roslyn.Suppressions, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' +- 'SonarLint.VisualStudio.RoslynAnalyzerServer, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'SonarLint.VisualStudio.SLCore, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'SonarQube.Client, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' @@ -107,12 +107,10 @@ Referenced assemblies: - 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.IO.Abstractions, Version=9.0.0.0, Culture=neutral, PublicKeyToken=96bf224d23c43e59' - 'System.Net.Http, Version=4.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' -- 'System.Threading.Channels, Version=7.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' -- 'System.Threading.Tasks.Extensions, Version=4.2.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' - 'System.Xaml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'WindowsBase, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' -# Number of references: 28 +# Number of references: 26 --- Assembly: 'SonarLint.VisualStudio.Core, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' @@ -164,12 +162,9 @@ Assembly: 'SonarLint.VisualStudio.Infrastructure.VS, Version=8.30.0.0, Culture=n Relative path: 'SonarLint.VisualStudio.Infrastructure.VS.dll' Referenced assemblies: -- 'Microsoft.CodeAnalysis, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' -- 'Microsoft.CodeAnalysis.Workspaces, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' - 'Microsoft.VisualStudio.CoreUtility, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'Microsoft.VisualStudio.Editor, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'Microsoft.VisualStudio.Interop, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' -- 'Microsoft.VisualStudio.LanguageServices, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' - 'Microsoft.VisualStudio.Shell.15.0, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'Microsoft.VisualStudio.Shell.Framework, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'Microsoft.VisualStudio.Text.Data, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' @@ -177,12 +172,11 @@ Referenced assemblies: - 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'SonarLint.VisualStudio.Core, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' -- 'System.Collections.Immutable, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'System.ComponentModel.Composition, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'System.IO.Abstractions, Version=9.0.0.0, Culture=neutral, PublicKeyToken=96bf224d23c43e59' -# Number of references: 18 +# Number of references: 14 --- Assembly: 'SonarLint.VisualStudio.Integration, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' @@ -285,23 +279,23 @@ Referenced assemblies: # Number of references: 22 --- -Assembly: 'SonarLint.VisualStudio.Roslyn.Suppressions, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' -Relative path: 'SonarLint.VisualStudio.Roslyn.Suppressions.dll' +Assembly: 'SonarLint.VisualStudio.RoslynAnalyzerServer, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' +Relative path: 'SonarLint.VisualStudio.RoslynAnalyzerServer.dll' Referenced assemblies: - 'Microsoft.CodeAnalysis, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' -- 'Microsoft.VisualStudio.Threading, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' +- 'Microsoft.CodeAnalysis.Workspaces, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' +- 'Microsoft.VisualStudio.LanguageServices, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' +- 'Microsoft.VisualStudio.Threading, Version=16.10.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed' -- 'SonarLint.VisualStudio.ConnectedMode, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'SonarLint.VisualStudio.Core, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' -- 'SonarLint.VisualStudio.Infrastructure.VS, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' -- 'SonarQube.Client, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Collections.Immutable, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'System.ComponentModel.Composition, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' -- 'System.IO.Abstractions, Version=9.0.0.0, Culture=neutral, PublicKeyToken=96bf224d23c43e59' +- 'System.Net.Http, Version=4.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' +- 'System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' # Number of references: 13 --- @@ -331,25 +325,25 @@ Referenced assemblies: - 'SonarLint.VisualStudio.Core, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'SonarLint.VisualStudio.IssueVisualization, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'SonarLint.VisualStudio.IssueVisualization.Security, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' +- 'SonarLint.VisualStudio.RoslynAnalyzerServer, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'SonarLint.VisualStudio.SLCore, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Collections.Immutable, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'System.ComponentModel.Composition, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' -# Number of references: 10 +# Number of references: 11 --- Assembly: 'SonarQube.Client, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' Relative path: 'SonarQube.Client.dll' Referenced assemblies: -- 'Google.Protobuf, Version=3.6.1.0, Culture=neutral, PublicKeyToken=a7d26565bac4d604' - 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed' - 'SonarLint.VisualStudio.Core, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Net.Http, Version=4.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' -# Number of references: 7 +# Number of references: 6 ... diff --git a/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithoutStrongNames.txt b/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithoutStrongNames.txt index 58862f0d78..b84e1a929f 100644 --- a/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithoutStrongNames.txt +++ b/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithoutStrongNames.txt @@ -1,7 +1,7 @@ --- ################################ # Assembly references report -# Report date/time: 2025-10-14T13:19:29.6557597Z +# Report date/time: 2025-10-14T14:24:04.3472038Z ################################ # # Generated by Devtility CheckAsmRefs v0.11.0.223 @@ -51,7 +51,7 @@ Referenced assemblies: - 'SonarLint.VisualStudio.Integration, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' - 'SonarLint.VisualStudio.IssueVisualization, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' - 'SonarLint.VisualStudio.IssueVisualization.Security, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' -- 'SonarLint.VisualStudio.Roslyn.Suppressions, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' +- 'SonarLint.VisualStudio.RoslynAnalyzerServer, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' - 'SonarLint.VisualStudio.SLCore, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' - 'SonarQube.Client, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' - 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' @@ -107,12 +107,10 @@ Referenced assemblies: - 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.IO.Abstractions, Version=9.0.0.0, Culture=neutral, PublicKeyToken=96bf224d23c43e59' - 'System.Net.Http, Version=4.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' -- 'System.Threading.Channels, Version=7.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' -- 'System.Threading.Tasks.Extensions, Version=4.2.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' - 'System.Xaml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'WindowsBase, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' -# Number of references: 28 +# Number of references: 26 --- Assembly: 'SonarLint.VisualStudio.Core, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' @@ -164,12 +162,9 @@ Assembly: 'SonarLint.VisualStudio.Infrastructure.VS, Version=8.30.0.0, Culture=n Relative path: 'SonarLint.VisualStudio.Infrastructure.VS.dll' Referenced assemblies: -- 'Microsoft.CodeAnalysis, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' -- 'Microsoft.CodeAnalysis.Workspaces, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' - 'Microsoft.VisualStudio.CoreUtility, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'Microsoft.VisualStudio.Editor, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'Microsoft.VisualStudio.Interop, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' -- 'Microsoft.VisualStudio.LanguageServices, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' - 'Microsoft.VisualStudio.Shell.15.0, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'Microsoft.VisualStudio.Shell.Framework, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'Microsoft.VisualStudio.Text.Data, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' @@ -177,12 +172,11 @@ Referenced assemblies: - 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'SonarLint.VisualStudio.Core, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' - 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' -- 'System.Collections.Immutable, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'System.ComponentModel.Composition, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'System.IO.Abstractions, Version=9.0.0.0, Culture=neutral, PublicKeyToken=96bf224d23c43e59' -# Number of references: 18 +# Number of references: 14 --- Assembly: 'SonarLint.VisualStudio.Integration, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' @@ -285,23 +279,23 @@ Referenced assemblies: # Number of references: 22 --- -Assembly: 'SonarLint.VisualStudio.Roslyn.Suppressions, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' -Relative path: 'SonarLint.VisualStudio.Roslyn.Suppressions.dll' +Assembly: 'SonarLint.VisualStudio.RoslynAnalyzerServer, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' +Relative path: 'SonarLint.VisualStudio.RoslynAnalyzerServer.dll' Referenced assemblies: - 'Microsoft.CodeAnalysis, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' -- 'Microsoft.VisualStudio.Threading, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' +- 'Microsoft.CodeAnalysis.Workspaces, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' +- 'Microsoft.VisualStudio.LanguageServices, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' +- 'Microsoft.VisualStudio.Threading, Version=16.10.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed' -- 'SonarLint.VisualStudio.ConnectedMode, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' - 'SonarLint.VisualStudio.Core, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' -- 'SonarLint.VisualStudio.Infrastructure.VS, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' -- 'SonarQube.Client, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' - 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Collections.Immutable, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'System.ComponentModel.Composition, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' -- 'System.IO.Abstractions, Version=9.0.0.0, Culture=neutral, PublicKeyToken=96bf224d23c43e59' +- 'System.Net.Http, Version=4.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' +- 'System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' # Number of references: 13 --- @@ -331,25 +325,25 @@ Referenced assemblies: - 'SonarLint.VisualStudio.Core, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' - 'SonarLint.VisualStudio.IssueVisualization, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' - 'SonarLint.VisualStudio.IssueVisualization.Security, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' +- 'SonarLint.VisualStudio.RoslynAnalyzerServer, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' - 'SonarLint.VisualStudio.SLCore, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' - 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Collections.Immutable, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'System.ComponentModel.Composition, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' -# Number of references: 10 +# Number of references: 11 --- Assembly: 'SonarQube.Client, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' Relative path: 'SonarQube.Client.dll' Referenced assemblies: -- 'Google.Protobuf, Version=3.6.1.0, Culture=neutral, PublicKeyToken=a7d26565bac4d604' - 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed' - 'SonarLint.VisualStudio.Core, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' - 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Net.Http, Version=4.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' -# Number of references: 7 +# Number of references: 6 ... diff --git a/src/Integration.Vsix/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocator.cs b/src/Integration.Vsix/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocator.cs index d309404ca1..24fba3f83c 100644 --- a/src/Integration.Vsix/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocator.cs +++ b/src/Integration.Vsix/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocator.cs @@ -21,38 +21,48 @@ using System.ComponentModel.Composition; using System.IO; using System.IO.Abstractions; -using SonarLint.VisualStudio.Infrastructure.VS.Roslyn; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.SystemAbstractions; using SonarLint.VisualStudio.Integration.Vsix.Helpers; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; namespace SonarLint.VisualStudio.Integration.Vsix.EmbeddedAnalyzers; [Export(typeof(IEmbeddedDotnetAnalyzersLocator))] [PartCreationPolicy(CreationPolicy.Shared)] -internal class EmbeddedDotnetAnalyzersLocator : IEmbeddedDotnetAnalyzersLocator +[method: ImportingConstructor] +internal class EmbeddedDotnetAnalyzersLocator(IVsixRootLocator vsixRootLocator, ILanguageProvider languageProvider, IFileSystemService fileSystem) + : IEmbeddedDotnetAnalyzersLocator { private const string PathInsideVsix = "EmbeddedDotnetAnalyzerDLLs"; private const string DllsSearchPattern = "SonarAnalyzer.*.dll"; // starting from 10.0, the analyzer assemblies are merged and all of the dll names start with SonarAnalyzer private const string EnterpriseInfix = ".Enterprise."; // enterprise analyzer assemblies are included in the same folder and need to be filtered out - private readonly IFileSystem fileSystem; - private readonly IVsixRootLocator vsixRootLocator; + private readonly IFileSystem fileSystem = fileSystem; - [ImportingConstructor] - public EmbeddedDotnetAnalyzersLocator(IVsixRootLocator vsixRootLocator) : this(vsixRootLocator, new FileSystem()) + public Dictionary> GetAnalyzerFullPathsByLicensedLanguage() { - } + var languageToDllsMap = new Dictionary>(); + var allAnalyzers = GetAllAnalyzerDlls(); + foreach (var roslynLanguage in languageProvider.RoslynLanguages) + { + var basicKey = new LicensedRoslynLanguage(roslynLanguage, IsEnterprise: false); + languageToDllsMap.Add(basicKey, GetAnalyzerFullPathsByLanguage(basicKey.RoslynLanguage, basicKey.IsEnterprise, allAnalyzers)); - internal EmbeddedDotnetAnalyzersLocator(IVsixRootLocator vsixRootLocator, IFileSystem fileSystem) - { - this.vsixRootLocator = vsixRootLocator; - this.fileSystem = fileSystem; + var enterpriseKey = new LicensedRoslynLanguage(roslynLanguage, IsEnterprise: true); + languageToDllsMap.Add(enterpriseKey, GetAnalyzerFullPathsByLanguage(enterpriseKey.RoslynLanguage, enterpriseKey.IsEnterprise, allAnalyzers)); + } + return languageToDllsMap; } - public List GetBasicAnalyzerFullPaths() => GetAnalyzerDlls().Where(x => !x.Contains(EnterpriseInfix)).ToList(); + private static List GetAnalyzerFullPathsByLanguage(RoslynLanguage language, bool shouldUseEnterprise, IEnumerable allAnalyzers) + { + var dlls = shouldUseEnterprise ? allAnalyzers : allAnalyzers.Where(x => !x.Contains(EnterpriseInfix)); - public List GetEnterpriseAnalyzerFullPaths() => GetAnalyzerDlls().ToList(); + return dlls.Where(dll => dll.Contains(language.RoslynDllIdentifier)).ToList(); + } - private string[] GetAnalyzerDlls() => fileSystem.Directory.GetFiles(GetPathToParentFolder(), DllsSearchPattern); + private string[] GetAllAnalyzerDlls() => fileSystem.Directory.GetFiles(GetPathToParentFolder(), DllsSearchPattern); private string GetPathToParentFolder() => Path.Combine(vsixRootLocator.GetVsixRoot(), PathInsideVsix); } diff --git a/src/Integration.Vsix/Integration.Vsix.csproj b/src/Integration.Vsix/Integration.Vsix.csproj index 78184e93cd..c211951346 100644 --- a/src/Integration.Vsix/Integration.Vsix.csproj +++ b/src/Integration.Vsix/Integration.Vsix.csproj @@ -110,12 +110,6 @@ BuiltProjectOutputGroup%3bBuiltProjectOutputGroupDependencies%3bGetCopyToOutputDirectoryItems%3bSatelliteDllsProjectOutputGroup%3b DebugSymbolsProjectOutputGroup%3b - - {082D5D8E-F914-4139-9AE3-3F48B679E3DA} - Roslyn.Suppressions - BuiltProjectOutputGroup%3bBuiltProjectOutputGroupDependencies%3bGetCopyToOutputDirectoryItems%3bSatelliteDllsProjectOutputGroup%3b - DebugSymbolsProjectOutputGroup%3b - {58619C0F-0F3D-4E8C-B204-A19B332D45E5} CFamily @@ -132,6 +126,11 @@ BuiltProjectOutputGroup%3bBuiltProjectOutputGroupDependencies%3bGetCopyToOutputDirectoryItems%3bSatelliteDllsProjectOutputGroup%3b DebugSymbolsProjectOutputGroup%3b + + RoslynAnalyzerServer + BuiltProjectOutputGroup%3bBuiltProjectOutputGroupDependencies%3bGetCopyToOutputDirectoryItems%3bSatelliteDllsProjectOutputGroup%3b + DebugSymbolsProjectOutputGroup%3b + diff --git a/src/Integration.Vsix/Manifests/VS2022/source.extension.vsixmanifest b/src/Integration.Vsix/Manifests/VS2022/source.extension.vsixmanifest index 71680086de..a439a6f975 100644 --- a/src/Integration.Vsix/Manifests/VS2022/source.extension.vsixmanifest +++ b/src/Integration.Vsix/Manifests/VS2022/source.extension.vsixmanifest @@ -39,19 +39,8 @@ + - - - - - - - - diff --git a/src/SonarQube.Client/Api/IGetIssuesRequest.cs b/src/Integration.Vsix/RoslynQuickFixes/RoslynQuickFixApplication.cs similarity index 55% rename from src/SonarQube.Client/Api/IGetIssuesRequest.cs rename to src/Integration.Vsix/RoslynQuickFixes/RoslynQuickFixApplication.cs index 08ad5c5e55..aed20122d0 100644 --- a/src/SonarQube.Client/Api/IGetIssuesRequest.cs +++ b/src/Integration.Vsix/RoslynQuickFixes/RoslynQuickFixApplication.cs @@ -18,30 +18,20 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using SonarQube.Client.Models; -using SonarQube.Client.Requests; +using Microsoft.VisualStudio.Text; +using SonarLint.VisualStudio.IssueVisualization.Models; +using SonarLint.VisualStudio.RoslynAnalyzerServer; -namespace SonarQube.Client.Api; +namespace SonarLint.VisualStudio.Integration.Vsix.RoslynQuickFixes; -interface IGetIssuesRequest : IRequest +public class RoslynQuickFixApplication(RoslynQuickFixApplicationImpl implementation) : IQuickFixApplication { - string ProjectKey { get; set; } + internal readonly RoslynQuickFixApplicationImpl Implementation = implementation; - string Statuses { get; set; } + public string Message => Implementation.Message; - /// - /// The branch name to fetch. - /// - /// If the value is null/empty, the main branch will be fetched - string Branch { get; set; } + public bool CanBeApplied(ITextSnapshot currentSnapshot) => true; - string[] IssueKeys { get; set; } - - string RuleId { get; set; } - - string ComponentKey { get; set; } - - string Languages { get; set; } - - // Update when adding properties here. + public async Task ApplyAsync(ITextSnapshot currentSnapshot, CancellationToken cancellationToken) => + await Implementation.ApplyAsync(cancellationToken); } diff --git a/src/Integration.Vsix/RoslynQuickFixes/RoslynQuickFixStorage.cs b/src/Integration.Vsix/RoslynQuickFixes/RoslynQuickFixStorage.cs new file mode 100644 index 0000000000..b422c21d7c --- /dev/null +++ b/src/Integration.Vsix/RoslynQuickFixes/RoslynQuickFixStorage.cs @@ -0,0 +1,84 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using SonarLint.VisualStudio.Core.ConfigurationScope; +using SonarLint.VisualStudio.IssueVisualization.Models; +using SonarLint.VisualStudio.RoslynAnalyzerServer; + +namespace SonarLint.VisualStudio.Integration.Vsix.RoslynQuickFixes; + +[Export(typeof(IRoslynQuickFixStorageWriter))] +[Export(typeof(IRoslynQuickFixProvider))] +[PartCreationPolicy(CreationPolicy.Shared)] +public class RoslynQuickFixStorage : IRoslynQuickFixStorageWriter, IRoslynQuickFixProvider +{ + private readonly object locker = new(); + private readonly Dictionary cache = new(); + + [method: ImportingConstructor] + public RoslynQuickFixStorage(IActiveConfigScopeTracker configScopeTracker) + { + configScopeTracker.CurrentConfigurationScopeChanged += ConfigScopeTracker_OnCurrentConfigurationScopeChanged; // it's okay to miss initial events here, this is only used for cache cleanup + } + + private void ConfigScopeTracker_OnCurrentConfigurationScopeChanged(object sender, ConfigurationScopeChangedEventArgs e) + { + if (!e.DefinitionChanged) + { + return; + } + + ClearCache(); + } + + private void ClearCache() // todo https://sonarsource.atlassian.net/browse/SLVS-2540 clear cache on analysis for file + { + lock (locker) + { + cache.Clear(); + } + } + + public void Add( + Guid id, + RoslynQuickFixApplicationImpl impl) + { + lock (locker) + { + cache[id] = impl; + } + } + + public bool TryGet(Guid id, out IQuickFixApplication roslynQuickFix) + { + lock (locker) + { + if (cache.TryGetValue(id, out var quickFixImplementation)) + { + roslynQuickFix = new RoslynQuickFixApplication(quickFixImplementation); + return true; + } + } + + roslynQuickFix = null; + return false; + } +} diff --git a/src/Integration.Vsix/SLCore/SLCoreEmbeddedPluginJarLocator.cs b/src/Integration.Vsix/SLCore/SLCoreEmbeddedPluginProvider.cs similarity index 75% rename from src/Integration.Vsix/SLCore/SLCoreEmbeddedPluginJarLocator.cs rename to src/Integration.Vsix/SLCore/SLCoreEmbeddedPluginProvider.cs index 1984bc620f..dafc53336e 100644 --- a/src/Integration.Vsix/SLCore/SLCoreEmbeddedPluginJarLocator.cs +++ b/src/Integration.Vsix/SLCore/SLCoreEmbeddedPluginProvider.cs @@ -26,25 +26,28 @@ using SonarLint.VisualStudio.Integration.Vsix.Helpers; using SonarLint.VisualStudio.Integration.Vsix.Resources; using SonarLint.VisualStudio.SLCore.Configuration; +using IFileSystem = System.IO.Abstractions.IFileSystem; +using ILogger = SonarLint.VisualStudio.Core.ILogger; namespace SonarLint.VisualStudio.Integration.Vsix.SLCore; -[Export(typeof(ISLCoreEmbeddedPluginJarLocator))] +[Export(typeof(ISLCoreEmbeddedPluginProvider))] [PartCreationPolicy(CreationPolicy.Shared)] -public class SLCoreEmbeddedPluginJarLocator : ISLCoreEmbeddedPluginJarLocator +public class SLCoreEmbeddedPluginProvider : ISLCoreEmbeddedPluginProvider { private const string JarFolderName = "DownloadedJars"; private readonly IFileSystem fileSystem; private readonly ILogger logger; + private readonly ILanguageProvider languageProvider; private readonly IVsixRootLocator vsixRootLocator; internal HashSet StandalonePlugins { get; } [ImportingConstructor] - public SLCoreEmbeddedPluginJarLocator(IVsixRootLocator vsixRootLocator, ILogger logger, ILanguageProvider languageProvider) : this(vsixRootLocator, new FileSystem(), logger, languageProvider) { } + public SLCoreEmbeddedPluginProvider(IVsixRootLocator vsixRootLocator, ILogger logger, ILanguageProvider languageProvider) : this(vsixRootLocator, new FileSystem(), logger, languageProvider) { } - internal SLCoreEmbeddedPluginJarLocator( + internal SLCoreEmbeddedPluginProvider( IVsixRootLocator vsixRootLocator, IFileSystem fileSystem, ILogger logger, @@ -53,7 +56,8 @@ internal SLCoreEmbeddedPluginJarLocator( this.vsixRootLocator = vsixRootLocator; this.fileSystem = fileSystem; this.logger = logger; - StandalonePlugins = languageProvider.LanguagesInStandaloneMode.Except(languageProvider.RoslynLanguages).Select(x => x.PluginInfo).ToHashSet(); + this.languageProvider = languageProvider; + StandalonePlugins = languageProvider.LanguagesInStandaloneMode.Select(x => x.PluginInfo).ToHashSet(); } public List ListJarFiles() @@ -83,9 +87,16 @@ public Dictionary ListConnectedModeEmbeddedPluginPathsByKey() return connectedModeEmbeddedPluginPathsByKey; } + public List ListDisabledPluginKeysForAnalysis() + { + var allPlugins = languageProvider.LanguagesInStandaloneMode.Where(x => x.AdditionalPlugins != null).SelectMany(x => x.AdditionalPlugins).ToHashSet(); + allPlugins.UnionWith(StandalonePlugins); + return allPlugins.Where(p => !p.IsEnabledForAnalysis).Select(p => p.Key).ToList(); + } + private string GetPathByPluginKey(List pluginFilePaths, string pluginKey, string pluginNameRegexPattern) { - var regex = new Regex(pluginNameRegexPattern); + var regex = new Regex(pluginNameRegexPattern, RegexOptions.None, RegexConstants.DefaultTimeout); var matchedFilePaths = pluginFilePaths.Where(jar => regex.IsMatch(jar)).ToList(); switch (matchedFilePaths.Count) { diff --git a/src/Integration.Vsix/SonarLintDaemonPackage.cs b/src/Integration.Vsix/SonarLintDaemonPackage.cs index 50b9f1b371..96f42ee149 100644 --- a/src/Integration.Vsix/SonarLintDaemonPackage.cs +++ b/src/Integration.Vsix/SonarLintDaemonPackage.cs @@ -27,15 +27,14 @@ using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Analysis; using SonarLint.VisualStudio.Core.CFamily; -using SonarLint.VisualStudio.Infrastructure.VS.Roslyn; -using SonarLint.VisualStudio.Integration.CSharpVB.Install; using SonarLint.VisualStudio.Integration.Vsix.Analysis; -using SonarLint.VisualStudio.Integration.Vsix.CFamily; using SonarLint.VisualStudio.Integration.Vsix.Events; using SonarLint.VisualStudio.Integration.Vsix.Resources; +using SonarLint.VisualStudio.RoslynAnalyzerServer; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; using SonarLint.VisualStudio.SLCore; using SonarLint.VisualStudio.SLCore.Analysis; -using ErrorHandler = Microsoft.VisualStudio.ErrorHandler; +using ErrorHandler = SonarLint.VisualStudio.Core.ErrorHandler; namespace SonarLint.VisualStudio.Integration.Vsix { @@ -68,12 +67,12 @@ public sealed class SonarLintDaemonPackage : AsyncPackage private ILogger logger; private IActiveCompilationDatabaseTracker activeCompilationDatabaseTracker; - private ISolutionRoslynAnalyzerManager solutionRoslynAnalyzerManager; private IProjectDocumentsEventsListener projectDocumentsEventsListener; private ISLCoreHandler slCoreHandler; private IDocumentEventsHandler documentEventsHandler; private ISlCoreUserAnalysisPropertiesSynchronizer slCoreUserAnalysisPropertiesSynchronizer; private IAnalysisConfigMonitor analysisConfigMonitor; + private IRoslynAnalysisHttpServer roslynAnalysisHttpServer; /// /// Initializes a new instance of the class. @@ -120,11 +119,11 @@ private async Task InitAsync() projectDocumentsEventsListener = await this.GetMefServiceAsync(); projectDocumentsEventsListener.Initialize(); - solutionRoslynAnalyzerManager = await this.GetMefServiceAsync(); - var importBeforeFileGenerator = await this.GetMefServiceAsync(); - importBeforeFileGenerator.UpdateOrCreateTargetsFileAsync().Forget(); - - LegacyInstallationCleanup.CleanupDaemonFiles(logger); + var thread = await this.GetMefServiceAsync(); + var roslynAnalyzerAssemblyLoader = await this.GetMefServiceAsync(); + roslynAnalysisHttpServer = await this.GetMefServiceAsync(); + thread.RunOnBackgroundThread(() => roslynAnalyzerAssemblyLoader.LoadRoslynAnalyzerAssemblyContentsIfNeeded()).Forget(); + thread.RunOnBackgroundThread(() => StartRoslynAnalysisHttpServerAsync().ConfigureAwait(false)).Forget(); slCoreHandler = await this.GetMefServiceAsync(); slCoreHandler.EnableSloop(); @@ -142,6 +141,8 @@ private async Task MigrateBindingsToServerConnectionsIfNeededAsync() await bindingToConnectionMigration.MigrateAllBindingsToServerConnectionsIfNeededAsync(); } + private async Task StartRoslynAnalysisHttpServerAsync() => await roslynAnalysisHttpServer.StartListenAsync(); + protected override void Dispose(bool disposing) { base.Dispose(disposing); @@ -161,10 +162,11 @@ protected override void Dispose(bool disposing) projectDocumentsEventsListener?.Dispose(); projectDocumentsEventsListener = null; - solutionRoslynAnalyzerManager?.Dispose(); - solutionRoslynAnalyzerManager = null; slCoreHandler?.Dispose(); slCoreHandler = null; + + roslynAnalysisHttpServer.Dispose(); + roslynAnalysisHttpServer = null; } } diff --git a/src/Integration.Vsix/SonarLintIntegrationPackage.cs b/src/Integration.Vsix/SonarLintIntegrationPackage.cs index 21e7d1487d..a71554bc0c 100644 --- a/src/Integration.Vsix/SonarLintIntegrationPackage.cs +++ b/src/Integration.Vsix/SonarLintIntegrationPackage.cs @@ -26,7 +26,6 @@ using SonarLint.VisualStudio.Infrastructure.VS; using SonarLint.VisualStudio.Integration.Vsix.Resources; using SonarLint.VisualStudio.Integration.Vsix.Settings.FileExclusions; -using SonarLint.VisualStudio.Roslyn.Suppressions.InProcess; using ErrorHandler = Microsoft.VisualStudio.ErrorHandler; namespace SonarLint.VisualStudio.Integration.Vsix @@ -65,7 +64,6 @@ public class SonarLintIntegrationPackage : AsyncPackage private PackageCommandManager commandManager; private ILogger logger; - private IRoslynSettingsFileSynchronizer roslynSettingsFileSynchronizer; protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) { @@ -88,7 +86,6 @@ private async Task InitOnUIThreadAsync() await commandManager.InitializeAsync(this, ShowOptionPage); // make sure roslynSettingsFileSynchronizer is initialized - roslynSettingsFileSynchronizer = await this.GetMefServiceAsync(); Debug.Assert(threadHandling.CheckAccess(), "Still expecting to be on the UI thread"); logger.WriteLine(Strings.SL_InitializationComplete); @@ -99,15 +96,5 @@ private async Task InitOnUIThreadAsync() logger.WriteLine(Strings.SL_ERROR, ex.Message); } } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (disposing) - { - roslynSettingsFileSynchronizer?.Dispose(); - roslynSettingsFileSynchronizer = null; - } - } } } diff --git a/src/SonarQube.Client/Models/SonarQubeCleanCodeTaxonomy.cs b/src/Integration.Vsix/SonarLintTagger/ResettableOneShotTimer.cs similarity index 53% rename from src/SonarQube.Client/Models/SonarQubeCleanCodeTaxonomy.cs rename to src/Integration.Vsix/SonarLintTagger/ResettableOneShotTimer.cs index 890137f6fa..1062337dac 100644 --- a/src/SonarQube.Client/Models/SonarQubeCleanCodeTaxonomy.cs +++ b/src/Integration.Vsix/SonarLintTagger/ResettableOneShotTimer.cs @@ -18,46 +18,31 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -namespace SonarQube.Client.Models +namespace SonarLint.VisualStudio.Integration.Vsix.SonarLintTagger; + +internal interface IResettableOneShotTimer : IDisposable { - public enum SonarQubeCleanCodeAttribute - { - // Consistency - Conventional, - Formatted, - Identifiable, - - // Intentionality - Clear, - Complete, - Efficient, - Logical, - - // Adaptability - Distinct, - Focused, - Modular, - Tested, - - // Responsibility - Lawful, - Respectful, - Trustworthy - } + void Reset(); - public enum SonarQubeSoftwareQuality - { - Maintainability, - Reliability, - Security - } + event EventHandler Elapsed; +} - public enum SonarQubeSoftwareQualitySeverity +internal sealed class ResettableOneShotTimer : IResettableOneShotTimer +{ + private readonly Timer timer; + private readonly long timerDurationMs; + + public ResettableOneShotTimer(TimeSpan timerTimeSpan) { - Info = 0, - Low = 1, - Medium = 2, - High = 3, - Blocker = 4 + timerDurationMs = (long)timerTimeSpan.TotalMilliseconds; + timer = new Timer(TimerAction, null, Timeout.Infinite, Timeout.Infinite); } + + private void TimerAction(object state) => Elapsed?.Invoke(this, EventArgs.Empty); + + public void Reset() => timer.Change(timerDurationMs, Timeout.Infinite); + + public event EventHandler Elapsed; + + public void Dispose() => timer?.Dispose(); } diff --git a/src/Integration.Vsix/SonarLintTagger/TaggerProvider.cs b/src/Integration.Vsix/SonarLintTagger/TaggerProvider.cs index 111d69d854..37f438076e 100644 --- a/src/Integration.Vsix/SonarLintTagger/TaggerProvider.cs +++ b/src/Integration.Vsix/SonarLintTagger/TaggerProvider.cs @@ -40,10 +40,10 @@ namespace SonarLint.VisualStudio.Integration.Vsix; /// -/// Factory for the . There will be one instance of this class/VS session. +/// Factory for the . There will be one instance of this class/VS session. /// /// -/// See the README.md in this folder for more information +/// See the README.md in this folder for more information /// [Export(typeof(ITaggerProvider))] [Export(typeof(IDocumentTracker))] @@ -54,7 +54,9 @@ namespace SonarLint.VisualStudio.Integration.Vsix; internal sealed class TaggerProvider : ITaggerProvider, IRequireInitialization, IDocumentTracker, IDisposable { internal static readonly Type SingletonManagerPropertyCollectionKey = typeof(SingletonDisposableTaggerManager); + private readonly IAnalysisRequester analysisRequester; private readonly IAnalyzer analyzer; + private readonly TimeSpan debounceMilliseconds = TimeSpan.FromMilliseconds(500); private readonly IFileTracker fileTracker; private readonly IIssueConsumerFactory issueConsumerFactory; private readonly IIssueConsumerStorage issueConsumerStorage; @@ -62,21 +64,22 @@ internal sealed class TaggerProvider : ITaggerProvider, IRequireInitialization, private readonly ISet issueTrackers = new HashSet(); private readonly ISonarLanguageRecognizer languageRecognizer; - private readonly IAnalysisRequester analysisRequester; private readonly ILogger logger; private readonly object reanalysisLockObject = new(); internal readonly ISonarErrorListDataSource sonarErrorDataSource; private readonly ITaggableBufferIndicator taggableBufferIndicator; + private readonly ITaskExecutorWithDebounceFactory taskExecutorWithDebounceFactory; internal readonly ITextDocumentFactoryService textDocumentFactoryService; private readonly IVsProjectInfoProvider vsProjectInfoProvider; - private IVsStatusbar vsStatusBar; + + private bool disposed; private Guid? lastAnalysisId; private CancellableJobRunner reanalysisJob; private StatusBarReanalysisProgressHandler reanalysisProgressHandler; - - private bool disposed; + private IVsStatusbar vsStatusBar; + private readonly ITaskExecutorWithDebounce requestAnalysisDebounceExecutor; internal IEnumerable ActiveTrackersForTesting => issueTrackers; @@ -94,7 +97,8 @@ internal TaggerProvider( IFileTracker fileTracker, IAnalyzer analyzer, ILogger logger, - IInitializationProcessorFactory initializationProcessorFactory) + IInitializationProcessorFactory initializationProcessorFactory, + ITaskExecutorWithDebounceFactory taskExecutorWithDebounceFactory) { this.sonarErrorDataSource = sonarErrorDataSource; this.textDocumentFactoryService = textDocumentFactoryService; @@ -107,6 +111,8 @@ internal TaggerProvider( this.fileTracker = fileTracker; this.analyzer = analyzer; this.logger = logger; + this.taskExecutorWithDebounceFactory = taskExecutorWithDebounceFactory; + requestAnalysisDebounceExecutor = taskExecutorWithDebounceFactory.Create(debounceMilliseconds); InitializationProcessor = initializationProcessorFactory.CreateAndStart( [], @@ -117,12 +123,28 @@ internal TaggerProvider( })); } - public IInitializationProcessor InitializationProcessor { get; private set; } - - private void OnAnalysisRequested(object sender, AnalysisRequestEventArgs args) + public void Dispose() { - // This method is not currently used, but is left here as there are opportunities to use it in the future + if (disposed) + { + return; + } + + if (InitializationProcessor.IsFinalized) + { + analysisRequester.AnalysisRequested -= OnAnalysisRequested; + } + + disposed = true; + } + public IInitializationProcessor InitializationProcessor { get; } + + private void OnAnalysisRequested(object sender, AnalysisRequestEventArgs args) => + requestAnalysisDebounceExecutor.Debounce(() => RequestAnalysis(args)); + + private void RequestAnalysis(AnalysisRequestEventArgs args) + { lock (reanalysisLockObject) { reanalysisJob?.Cancel(); @@ -176,7 +198,7 @@ private void NotifyFileTracker(IEnumerable filteredIssueTrackers) #region IViewTaggerProvider members /// - /// Create a tagger that will track SonarLint issues on the view/buffer combination. + /// Create a tagger that will track SonarLint issues on the view/buffer combination. /// public ITagger CreateTagger(ITextBuffer buffer) where T : ITag { @@ -216,6 +238,7 @@ private TextBufferIssueTracker InternalCreateTextBufferIssueTracker(ITextDocumen vsProjectInfoProvider, issueConsumerFactory, issueConsumerStorage, + taskExecutorWithDebounceFactory.Create(debounceMilliseconds), logger); #endregion IViewTaggerProvider members @@ -223,8 +246,9 @@ private TextBufferIssueTracker InternalCreateTextBufferIssueTracker(ITextDocumen #region IDocumentTracker methods public event EventHandler DocumentClosed; - public event EventHandler DocumentOpened; - public event EventHandler DocumentSaved; + public event EventHandler DocumentOpened; + public event EventHandler DocumentSaved; + public event EventHandler DocumentUpdated; public event EventHandler OpenDocumentRenamed; public Document[] GetOpenDocuments() @@ -247,7 +271,7 @@ public void AddIssueTracker(IIssueTracker issueTracker) NotifyFileTracker(filePath, content); // The lifetime of an issue tracker is tied to a single document. A document is opened, then a tracker is created. - DocumentOpened?.Invoke(this, new DocumentOpenedEventArgs(new Document(filePath, issueTracker.DetectedLanguages), content)); + DocumentOpened?.Invoke(this, new DocumentEventArgs(new Document(filePath, issueTracker.DetectedLanguages), content)); } public void OnOpenDocumentRenamed(string newFilePath, string oldFilePath, IEnumerable detectedLanguages) => @@ -256,7 +280,13 @@ public void OnOpenDocumentRenamed(string newFilePath, string oldFilePath, IEnume public void OnDocumentSaved(string fullPath, string newContent, IEnumerable detectedLanguages) { NotifyFileTracker(fullPath, newContent); - DocumentSaved?.Invoke(this, new DocumentSavedEventArgs(new Document(fullPath, detectedLanguages), newContent)); + DocumentSaved?.Invoke(this, new DocumentEventArgs(new Document(fullPath, detectedLanguages), newContent)); + } + + public void OnDocumentUpdated(string fullPath, string newContent, IEnumerable detectedLanguages) + { + NotifyFileTracker(fullPath, newContent); + DocumentUpdated?.Invoke(this, new DocumentEventArgs(new Document(fullPath, detectedLanguages), newContent)); } public void OnDocumentClosed(IIssueTracker issueTracker) @@ -272,19 +302,4 @@ public void OnDocumentClosed(IIssueTracker issueTracker) } #endregion IDocumentTracker methods - - public void Dispose() - { - if (disposed) - { - return; - } - - if (InitializationProcessor.IsFinalized) - { - analysisRequester.AnalysisRequested -= OnAnalysisRequested; - } - - disposed = true; - } } diff --git a/src/Integration.Vsix/SonarLintTagger/TaskExecutorWithDebounce.cs b/src/Integration.Vsix/SonarLintTagger/TaskExecutorWithDebounce.cs new file mode 100644 index 0000000000..40c51eef4b --- /dev/null +++ b/src/Integration.Vsix/SonarLintTagger/TaskExecutorWithDebounce.cs @@ -0,0 +1,88 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using Microsoft.VisualStudio.Threading; +using SonarLint.VisualStudio.Core; + +namespace SonarLint.VisualStudio.Integration.Vsix.SonarLintTagger; + +internal interface ITaskExecutorWithDebounceFactory +{ + ITaskExecutorWithDebounce Create(TimeSpan debounceTimeSpan); +} + +internal interface ITaskExecutorWithDebounce : IDisposable +{ + void Debounce(Action task); +} + +[Export(typeof(ITaskExecutorWithDebounceFactory))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +internal class TaskExecutorWithDebounceFactory(IThreadHandling threadHandling) : ITaskExecutorWithDebounceFactory +{ + public ITaskExecutorWithDebounce Create(TimeSpan debounceTimeSpan) => new TaskExecutorWithDebounce(new ResettableOneShotTimer(debounceTimeSpan), threadHandling); +} + +internal sealed class TaskExecutorWithDebounce : ITaskExecutorWithDebounce +{ + private readonly IThreadHandling threadHandling; + private readonly object locker = new(); + private readonly IResettableOneShotTimer timer; + private Action latestDebounceState; + + internal TaskExecutorWithDebounce(IResettableOneShotTimer timerWrapper, IThreadHandling threadHandling) + { + this.threadHandling = threadHandling; + timer = timerWrapper; + timer.Elapsed += DebounceAction; + } + + private void DebounceAction(object state, EventArgs eventArgs) + { + Action action; + lock (locker) + { + action = latestDebounceState; + latestDebounceState = null; + } + + if (action != null) + { + threadHandling.RunOnBackgroundThread(action).Forget(); + } + } + + public void Debounce(Action task) + { + lock (locker) + { + latestDebounceState = task; + timer.Reset(); + } + } + + public void Dispose() + { + timer.Elapsed -= DebounceAction; + timer.Dispose(); + } +} diff --git a/src/Integration.Vsix/SonarLintTagger/TextBufferIssueTracker.cs b/src/Integration.Vsix/SonarLintTagger/TextBufferIssueTracker.cs index ab3d0a9fbc..e7793b43a6 100644 --- a/src/Integration.Vsix/SonarLintTagger/TextBufferIssueTracker.cs +++ b/src/Integration.Vsix/SonarLintTagger/TextBufferIssueTracker.cs @@ -48,10 +48,12 @@ internal sealed class TextBufferIssueTracker : IIssueTracker, ITagger private readonly ITextDocument document; private readonly IIssueConsumerFactory issueConsumerFactory; private readonly IIssueConsumerStorage issueConsumerStorage; + private readonly ITaskExecutorWithDebounce taskExecutorWithDebounce; private readonly ILogger logger; private readonly ISonarErrorListDataSource sonarErrorDataSource; private readonly ITextBuffer textBuffer; private readonly IVsProjectInfoProvider vsProjectInfoProvider; + internal /* for testing */ TaggerProvider Provider { get; } internal /* for testing */ IssuesSnapshotFactory Factory { get; } @@ -63,6 +65,7 @@ public TextBufferIssueTracker( IVsProjectInfoProvider vsProjectInfoProvider, IIssueConsumerFactory issueConsumerFactory, IIssueConsumerStorage issueConsumerStorage, + ITaskExecutorWithDebounce taskExecutorWithDebounce, ILogger logger) { Provider = provider; @@ -72,6 +75,7 @@ public TextBufferIssueTracker( this.vsProjectInfoProvider = vsProjectInfoProvider; this.issueConsumerFactory = issueConsumerFactory; this.issueConsumerStorage = issueConsumerStorage; + this.taskExecutorWithDebounce = taskExecutorWithDebounce; this.logger = logger; logger.ForContext(nameof(TextBufferIssueTracker)); @@ -81,6 +85,10 @@ public TextBufferIssueTracker( Factory = new IssuesSnapshotFactory(LastAnalysisFilePath); document.FileActionOccurred += SafeOnFileActionOccurred; + if (textBuffer is ITextBuffer2 textBuffer2) + { + textBuffer2.ChangedOnBackground += TextBuffer_OnChangedOnBackground; + } sonarErrorDataSource.AddFactory(Factory); Provider.AddIssueTracker(this); @@ -91,18 +99,7 @@ public TextBufferIssueTracker( public string LastAnalysisFilePath { get; private set; } public IEnumerable DetectedLanguages { get; } - public void UpdateAnalysisState() - { - try - { - RemoveIssueConsumer(LastAnalysisFilePath); - InitializeAnalysisState(); - } - catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) - { - logger.WriteLine(Strings.Analysis_ErrorUpdatingAnalysisState, ex); - } - } + public void UpdateAnalysisState() => UpdateAnalysisState(null); public string GetText() => document.TextBuffer.CurrentSnapshot.GetText(); @@ -111,7 +108,12 @@ public void Dispose() RemoveIssueConsumer(LastAnalysisFilePath); document.FileActionOccurred -= SafeOnFileActionOccurred; textBuffer.Properties.RemoveProperty(TaggerProvider.SingletonManagerPropertyCollectionKey); + if (textBuffer is ITextBuffer2 textBuffer2) + { + textBuffer2.ChangedOnBackground -= TextBuffer_OnChangedOnBackground; + } sonarErrorDataSource.RemoveFactory(Factory); + taskExecutorWithDebounce.Dispose(); Provider.OnDocumentClosed(this); } @@ -151,6 +153,19 @@ private void SafeOnFileActionOccurred(object sender, TextDocumentFileActionEvent } } + private void UpdateAnalysisState(ITextSnapshot newTextSnapshot) + { + try + { + RemoveIssueConsumer(LastAnalysisFilePath); + InitializeAnalysisState(newTextSnapshot); + } + catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) + { + logger.WriteLine(Strings.Analysis_ErrorUpdatingAnalysisState, ex); + } + } + private void SnapToNewSnapshot(IIssuesSnapshot newSnapshot) { // Tell our factory to snap to a new snapshot. @@ -159,11 +174,11 @@ private void SnapToNewSnapshot(IIssuesSnapshot newSnapshot) sonarErrorDataSource.RefreshErrorList(Factory); } - private AnalysisSnapshot GetAnalysisSnapshot() => new(LastAnalysisFilePath, document.TextBuffer.CurrentSnapshot); + private AnalysisSnapshot GetAnalysisSnapshot(ITextSnapshot newTextSnapshot = null) => new(LastAnalysisFilePath, newTextSnapshot ?? document.TextBuffer.CurrentSnapshot); - private void InitializeAnalysisState() + private void InitializeAnalysisState(ITextSnapshot newTextSnapshot = null) { - var analysisSnapshot = GetAnalysisSnapshot(); + var analysisSnapshot = GetAnalysisSnapshot(newTextSnapshot); CreateIssueConsumer(analysisSnapshot); } @@ -174,12 +189,13 @@ private void CreateIssueConsumer(AnalysisSnapshot analysisSnapshot) var (projectName, projectGuid) = vsProjectInfoProvider.GetDocumentProjectInfo(analysisSnapshot.FilePath); var issueConsumer = issueConsumerFactory.Create(document, analysisSnapshot.FilePath, analysisSnapshot.TextSnapshot, projectName, projectGuid, SnapToNewSnapshot); issueConsumerStorage.Set(analysisSnapshot.FilePath, issueConsumer); - ClearErrorList(analysisSnapshot.FilePath, issueConsumer); } - private static void ClearErrorList(string filePath, IIssueConsumer issueConsumer) - { - issueConsumer.SetIssues(filePath, []); - issueConsumer.SetHotspots(filePath, []); - } + private void TextBuffer_OnChangedOnBackground(object sender, TextContentChangedEventArgs e) => + taskExecutorWithDebounce.Debounce(() => + { + var textSnapshot = e.After; + UpdateAnalysisState(textSnapshot); + Provider.OnDocumentUpdated(document.FilePath, textSnapshot.GetText(), DetectedLanguages); + }); } diff --git a/src/Integration.Vsix/app.config b/src/Integration.Vsix/app.config index a165d7d6f6..97fd28e340 100644 --- a/src/Integration.Vsix/app.config +++ b/src/Integration.Vsix/app.config @@ -62,10 +62,6 @@ - - - - diff --git a/src/Integration.Vsix/packages.lock.json b/src/Integration.Vsix/packages.lock.json index c3ad73928d..6828464143 100644 --- a/src/Integration.Vsix/packages.lock.json +++ b/src/Integration.Vsix/packages.lock.json @@ -190,16 +190,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "Humanizer.Core": { "type": "Transitive", "resolved": "2.2.0", @@ -1532,16 +1522,10 @@ "SonarLint.VisualStudio.IssueVisualization": "[1.0.0, )" } }, - "SonarLint.VisualStudio.Roslyn.Suppressions": { + "SonarLint.VisualStudio.RoslynAnalyzerServer": { "type": "Project", "dependencies": { - "Microsoft.VisualStudio.Sdk": "[17.0.31902.203, )", - "Newtonsoft.Json": "[13.0.3, )", - "SonarLint.VisualStudio.ConnectedMode": "[1.0.0, )", - "SonarLint.VisualStudio.Core": "[1.0.0, )", - "SonarLint.VisualStudio.Infrastructure.VS": "[1.0.0, )", - "SonarQube.Client": "[1.0.0, )", - "System.IO.Abstractions": "[9.0.4, )" + "SonarLint.VisualStudio.Core": "[1.0.0, )" } }, "SonarLint.VisualStudio.SLCore": { @@ -1556,14 +1540,13 @@ "dependencies": { "SonarLint.VisualStudio.ConnectedMode": "[1.0.0, )", "SonarLint.VisualStudio.IssueVisualization.Security": "[1.0.0, )", + "SonarLint.VisualStudio.RoslynAnalyzerServer": "[1.0.0, )", "SonarLint.VisualStudio.SLCore": "[1.0.0, )" } }, "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/Integration/CSharpVB/Install/ImportBeforeFileGenerator.cs b/src/Integration/CSharpVB/Install/ImportBeforeFileGenerator.cs deleted file mode 100644 index f97b64c057..0000000000 --- a/src/Integration/CSharpVB/Install/ImportBeforeFileGenerator.cs +++ /dev/null @@ -1,102 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using System.IO; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.SystemAbstractions; -using SonarLint.VisualStudio.Integration.Resources; - -namespace SonarLint.VisualStudio.Integration.CSharpVB.Install; - -/// -/// Creates a .targets file in the ImportBefore directory with the contents of the SonarLintTargets.xml file and updates it when the content becomes outdated. -/// -public interface IImportBeforeFileGenerator -{ - Task UpdateOrCreateTargetsFileAsync(); -} - -[Export(typeof(IImportBeforeFileGenerator))] -[PartCreationPolicy(CreationPolicy.Shared)] -[method: ImportingConstructor] -internal class ImportBeforeFileGenerator( - ILogger logger, - IFileSystemService fileSystem, - IThreadHandling threadHandling, - IEmbeddedResourceReader embeddedResourceReader) : IImportBeforeFileGenerator -{ - private readonly ILogger logger = logger.ForContext(Strings.ImportsBeforeFileGeneratorLogContext); - private const string TargetsFileName = "SonarLint.targets"; - private const string ResourcePath = "SonarLint.VisualStudio.Integration.CSharpVB.Install.SonarLintTargets.xml"; - - private static readonly object Locker = new(); - - public Task UpdateOrCreateTargetsFileAsync() => threadHandling.RunOnBackgroundThread(UpdateOrCreateTargetsFile); - - internal void UpdateOrCreateTargetsFile() - { - lock (Locker) - { - var fileContent = embeddedResourceReader.Read(GetType().Assembly, ResourcePath); - var pathToImportBefore = GetPathToImportBefore(); - var fullPath = Path.Combine(pathToImportBefore, TargetsFileName); - - logger.LogVerbose(Strings.ImportBeforeFileGenerator_CheckingIfFileExists, fullPath); - - try - { - if (fileContent == null) - { - logger.LogVerbose(Strings.ImportBeforeFileGenerator_ContentOfTargetsFileCanNotBeRead, TargetsFileName); - return; - } - - if (!fileSystem.Directory.Exists(pathToImportBefore)) - { - logger.LogVerbose(Strings.ImportBeforeFileGenerator_CreatingDirectory, pathToImportBefore); - fileSystem.Directory.CreateDirectory(pathToImportBefore); - } - - if (fileSystem.File.Exists(fullPath) && fileSystem.File.ReadAllText(fullPath) == fileContent) - { - logger.LogVerbose(Strings.ImportBeforeFileGenerator_FileAlreadyExists); - return; - } - - logger.LogVerbose(Strings.ImportBeforeFileGenerator_WritingTargetFileToDisk); - fileSystem.File.WriteAllText(fullPath, fileContent); - } - catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) - { - logger.WriteLine(Strings.ImportBeforeFileGenerator_FailedToWriteFile, ex.Message); - logger.LogVerbose(Strings.ImportBeforeFileGenerator_FailedToWriteFile_Verbose, ex.ToString()); - } - } - } - - private static string GetPathToImportBefore() - { - var localAppData = Environment.GetEnvironmentVariable("LOCALAPPDATA"); - var pathToImportBefore = Path.Combine(localAppData, "Microsoft", "MSBuild", "Current", "Microsoft.Common.targets", "ImportBefore"); - - return pathToImportBefore; - } -} diff --git a/src/Integration/CSharpVB/Install/SonarLintTargets.xml b/src/Integration/CSharpVB/Install/SonarLintTargets.xml deleted file mode 100644 index e718e5a342..0000000000 --- a/src/Integration/CSharpVB/Install/SonarLintTargets.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - <_SLVSConnectedModeFolder>$(APPDATA)\SonarLint for Visual Studio\Bindings\$(SolutionName) - <_SLVSStandaloneModeFolder>$(APPDATA)\SonarLint for Visual Studio\SolutionSettings\$(SolutionName) - <_SLVSStandaloneModeSettingsJson>$(_SLVSStandaloneModeFolder)\settings.json - - <_SLVSRootFolder>$(APPDATA)\SonarLint for Visual Studio\.global - <_SLVSRootFolder Condition=" Exists($(_SLVSStandaloneModeSettingsJson))">$(_SLVSStandaloneModeFolder) - <_SLVSRootFolder Condition=" Exists($(_SLVSConnectedModeFolder))">$(_SLVSConnectedModeFolder) - - - <_SonarLanguage>$(Language.ToLowerInvariant) - <_SonarLanguage Condition=" $(Language) == 'c#'">csharp - <_SonarLanguage Condition=" $(Language) == 'visualbasic'">vb - - <_SLVSGeneratedRoslynConfigFile>$(_SLVSRootFolder)\sonarlint_$(_SonarLanguage).globalconfig - <_SLVSGeneratedAdditionalFile>$(_SLVSRootFolder)\$(_SonarLanguage)\SonarLint.xml - - - - - - - false - - - diff --git a/src/Integration/CSharpVB/RoslynConfigGenerator.cs b/src/Integration/CSharpVB/RoslynConfigGenerator.cs deleted file mode 100644 index 481ecb8643..0000000000 --- a/src/Integration/CSharpVB/RoslynConfigGenerator.cs +++ /dev/null @@ -1,69 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using System.IO; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.CSharpVB; -using SonarLint.VisualStudio.Core.SystemAbstractions; - -namespace SonarLint.VisualStudio.Integration.CSharpVB; - -[Export(typeof(IRoslynConfigGenerator))] -[PartCreationPolicy(CreationPolicy.Shared)] -[method: ImportingConstructor] -internal class RoslynConfigGenerator( - IFileSystemService fileSystem, - IGlobalConfigGenerator globalConfigGenerator, - ISonarLintConfigGenerator sonarLintConfigGenerator, - ISonarLintConfigurationXmlSerializer sonarLintConfigurationXmlSerializer) - : IRoslynConfigGenerator -{ - /// - /// internal sonar-dotnet format used to provide rule parameters and file exclusions to the analyzer - /// - private const string SonarlintConfigFileName = "SonarLint.xml"; - - public void GenerateAndSaveConfiguration( - Language language, - string baseDirectory, - IDictionary properties, - IFileExclusions fileExclusions, - IReadOnlyCollection ruleStatuses, - IReadOnlyCollection ruleParameters) - { - var roslynGlobalConfig = globalConfigGenerator.Generate(ruleStatuses); - Save(roslynGlobalConfig, Path.Combine(baseDirectory, language.SettingsFileNameAndExtension)); - var sonarLintConfiguration = sonarLintConfigGenerator.Generate(ruleParameters, properties, fileExclusions, language); - Save(sonarLintConfigurationXmlSerializer.Serialize(sonarLintConfiguration), Path.Combine(baseDirectory, language.Id, SonarlintConfigFileName)); - } - - private void Save(string config, string filePath) - { - EnsureParentDirectoryExists(filePath); - fileSystem.File.WriteAllText(filePath, config); - } - - private void EnsureParentDirectoryExists(string filePath) - { - var parentDirectory = Path.GetDirectoryName(filePath); - fileSystem.Directory.CreateDirectory(parentDirectory); // will no-op if exists - } -} diff --git a/src/Integration/CSharpVB/StandaloneMode/StandaloneRoslynSettingsUpdater.cs b/src/Integration/CSharpVB/StandaloneMode/StandaloneRoslynSettingsUpdater.cs index d657cde620..259c012490 100644 --- a/src/Integration/CSharpVB/StandaloneMode/StandaloneRoslynSettingsUpdater.cs +++ b/src/Integration/CSharpVB/StandaloneMode/StandaloneRoslynSettingsUpdater.cs @@ -19,10 +19,7 @@ */ using System.ComponentModel.Composition; -using System.IO; -using Microsoft.VisualStudio.Threading; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.CSharpVB; +using System.Diagnostics.CodeAnalysis; using SonarLint.VisualStudio.Core.UserRuleSettings; namespace SonarLint.VisualStudio.Integration.CSharpVB.StandaloneMode; @@ -35,74 +32,12 @@ public interface IStandaloneRoslynSettingsUpdater [Export(typeof(IStandaloneRoslynSettingsUpdater))] [PartCreationPolicy(CreationPolicy.Shared)] [method: ImportingConstructor] -internal class StandaloneRoslynSettingsUpdater( - IRoslynConfigGenerator generator, - ILanguageProvider languageProvider, - IThreadHandling threadHandling) +[ExcludeFromCodeCoverage] // todo https://sonarsource.atlassian.net/browse/SLVS-2420 +internal class StandaloneRoslynSettingsUpdater() : IStandaloneRoslynSettingsUpdater { - private readonly object lockObject = new(); - - public void Update(UserSettings settings) => - threadHandling - .RunOnBackgroundThread(() => UpdateInternal(settings)) - .Forget(); - - private void UpdateInternal(UserSettings settings) - { - lock (lockObject) - { - var exclusions = ConvertExclusions(settings); - var (ruleStatusesByLanguage, ruleParametersByLanguage) = ConvertRules(settings); - - foreach (var language in languageProvider.RoslynLanguages) - { - generator.GenerateAndSaveConfiguration( - language, - settings.BaseDirectory, - settings.AnalysisSettings.AnalysisProperties, - exclusions, - ruleStatusesByLanguage[language], - ruleParametersByLanguage[language]); - } - } - } - - private static StandaloneRoslynFileExclusions ConvertExclusions(UserSettings settings) + public void Update(UserSettings settings) { - var exclusions = new StandaloneRoslynFileExclusions(settings.AnalysisSettings); - return exclusions; - } - - private (Dictionary>, Dictionary>) ConvertRules(UserSettings settings) - { - var ruleStatusesByLanguage = InitializeForAllRoslynLanguages(); - var ruleParametersByLanguage = InitializeForAllRoslynLanguages(); - foreach (var analysisSettingsRule in settings.AnalysisSettings.Rules) - { - if (!SonarCompositeRuleId.TryParse(analysisSettingsRule.Key, out var ruleId) - || !languageProvider.RoslynLanguages.Contains(ruleId.Language)) - { - continue; - } - - ruleStatusesByLanguage[ruleId.Language] - .Add(new StandaloneRoslynRuleStatus(ruleId, analysisSettingsRule.Value.Level is RuleLevel.On)); - ruleParametersByLanguage[ruleId.Language] - .Add(new StandaloneRoslynRuleParameters(ruleId, analysisSettingsRule.Value.Parameters)); - } - return (ruleStatusesByLanguage, ruleParametersByLanguage); - } - - private Dictionary> InitializeForAllRoslynLanguages() - { - var dictionary = new Dictionary>(); - - foreach (var language in languageProvider.RoslynLanguages) - { - dictionary[language] = []; - } - - return dictionary; + // TODO by https://sonarsource.atlassian.net/browse/SLVS-2420 drop this class } } diff --git a/src/Integration/Integration.csproj b/src/Integration/Integration.csproj index 09467b5212..d05fec3f81 100644 --- a/src/Integration/Integration.csproj +++ b/src/Integration/Integration.csproj @@ -58,7 +58,6 @@ - diff --git a/src/Integration/MefServices/ActiveSolutionBoundTracker.cs b/src/Integration/MefServices/ActiveSolutionBoundTracker.cs index 9544fe3599..49a056ab78 100644 --- a/src/Integration/MefServices/ActiveSolutionBoundTracker.cs +++ b/src/Integration/MefServices/ActiveSolutionBoundTracker.cs @@ -50,7 +50,6 @@ internal sealed class ActiveSolutionBoundTracker : IActiveSolutionBoundTracker, private readonly IActiveSolutionTracker solutionTracker; private readonly IConfigurationProvider configurationProvider; private readonly ISonarQubeService sonarQubeService; - private readonly IBoundSolutionGitMonitor gitEventsMonitor; private readonly IConfigScopeUpdater configScopeUpdater; private readonly ILogger logger; private IVsMonitorSelection vsMonitorSelection; @@ -59,8 +58,6 @@ internal sealed class ActiveSolutionBoundTracker : IActiveSolutionBoundTracker, public event EventHandler PreSolutionBindingChanged; public event EventHandler SolutionBindingChanged; - public event EventHandler PreSolutionBindingUpdated; - public event EventHandler SolutionBindingUpdated; public BindingConfiguration CurrentConfiguration { get; private set; } [ImportingConstructor] @@ -69,20 +66,18 @@ public ActiveSolutionBoundTracker( IActiveSolutionTracker activeSolutionTracker, IConfigScopeUpdater configScopeUpdater, ILogger logger, - IBoundSolutionGitMonitor gitEventsMonitor, IConfigurationProvider configurationProvider, ISonarQubeService sonarQubeService, IInitializationProcessorFactory initializationProcessorFactory) { this.serviceProvider = serviceProvider; solutionTracker = activeSolutionTracker; - this.gitEventsMonitor = gitEventsMonitor; this.logger = logger; this.configurationProvider = configurationProvider; this.sonarQubeService = sonarQubeService; this.configScopeUpdater = configScopeUpdater; InitializationProcessor = initializationProcessorFactory.Create( - [solutionTracker, this.gitEventsMonitor], + [solutionTracker], InitializeInternalAsync); CurrentConfiguration = BindingConfiguration.Standalone; @@ -107,7 +102,6 @@ await threadHandling.RunOnUIThreadAsync(() => return; } solutionTracker.ActiveSolutionChanged += OnActiveSolutionChanged; - gitEventsMonitor.HeadChanged += GitEventsMonitor_HeadChanged; } public void HandleBindingChange() @@ -121,15 +115,6 @@ public void HandleBindingChange() RaiseAnalyzersChangedIfBindingChanged(newBindingConfiguration); } - private void GitEventsMonitor_HeadChanged(object sender, EventArgs e) - { - if (CurrentConfiguration.Mode.IsInAConnectedMode()) - { - PreSolutionBindingUpdated?.Invoke(this, EventArgs.Empty); - SolutionBindingUpdated?.Invoke(this, EventArgs.Empty); - } - } - private async void OnActiveSolutionChanged(object sender, ActiveSolutionChangedEventArgs args) { // An exception here will crash VS @@ -149,8 +134,6 @@ private async Task HandleActiveSolutionChangeAsync() var connectionUpdatedSuccessfully = await UpdateConnectionAsync(newBindingConfiguration); - gitEventsMonitor.Refresh(); - RaiseAnalyzersChangedIfBindingChanged(connectionUpdatedSuccessfully ? newBindingConfiguration : BindingConfiguration.Standalone); } @@ -220,7 +203,6 @@ public void Dispose() if (InitializationProcessor.IsFinalized) { solutionTracker.ActiveSolutionChanged -= OnActiveSolutionChanged; - gitEventsMonitor.HeadChanged -= GitEventsMonitor_HeadChanged; } disposed = true; } diff --git a/src/Integration/packages.lock.json b/src/Integration/packages.lock.json index a541b4f223..7ad708a3ef 100644 --- a/src/Integration/packages.lock.json +++ b/src/Integration/packages.lock.json @@ -160,16 +160,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -1199,8 +1189,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/IssueViz.Security.UnitTests/packages.lock.json b/src/IssueViz.Security.UnitTests/packages.lock.json index 57c42a196f..a20214c256 100644 --- a/src/IssueViz.Security.UnitTests/packages.lock.json +++ b/src/IssueViz.Security.UnitTests/packages.lock.json @@ -111,16 +111,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -1341,8 +1331,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/IssueViz.Security/packages.lock.json b/src/IssueViz.Security/packages.lock.json index 815fb79e24..c57fd83523 100644 --- a/src/IssueViz.Security/packages.lock.json +++ b/src/IssueViz.Security/packages.lock.json @@ -137,16 +137,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -1199,8 +1189,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/IssueViz.UnitTests/AnalysisIssueVisualizationConverterTests.cs b/src/IssueViz.UnitTests/AnalysisIssueVisualizationConverterTests.cs index a7bfe7489a..52c492f40b 100644 --- a/src/IssueViz.UnitTests/AnalysisIssueVisualizationConverterTests.cs +++ b/src/IssueViz.UnitTests/AnalysisIssueVisualizationConverterTests.cs @@ -22,8 +22,10 @@ using Microsoft.VisualStudio.Text; using Moq; using SonarLint.VisualStudio.Core.Analysis; +using SonarLint.VisualStudio.Infrastructure.VS; using SonarLint.VisualStudio.IssueVisualization.Editor; using SonarLint.VisualStudio.IssueVisualization.Models; +using SonarLint.VisualStudio.TestInfrastructure; namespace SonarLint.VisualStudio.IssueVisualization.UnitTests; @@ -34,16 +36,28 @@ public class AnalysisIssueVisualizationConverterTests private ITextSnapshot textSnapshotMock; private AnalysisIssueVisualizationConverter testSubject; + private IRoslynQuickFixProvider roslynQuickFixProvider; [TestInitialize] public void TestInitialize() { issueSpanCalculatorMock = new Mock(); textSnapshotMock = Mock.Of(); + roslynQuickFixProvider = Substitute.For(); - testSubject = new AnalysisIssueVisualizationConverter(issueSpanCalculatorMock.Object); + testSubject = new AnalysisIssueVisualizationConverter(issueSpanCalculatorMock.Object, Substitute.For(), roslynQuickFixProvider); } + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + [TestMethod] public void Convert_NoTextBuffer_IssueWithNullSpan() { @@ -73,7 +87,7 @@ public void Convert_NoTextBuffer_SecondaryLocationSpansAreNotCalculated() [TestMethod] public void Convert_NoTextBuffer_QuickFixSpansAreNotCalculated() { - var issue = CreateIssue(Mock.Of()); + var issue = CreateIssue(Mock.Of()); var result = testSubject.Convert(issue, textSnapshot: null); @@ -110,8 +124,8 @@ public void Convert_IssueHasQuickFixes_QuickFixesSpansAreCalculated() SetupSpanCalculator(textRange3, span3); var edit3 = CreateEdit(textRange3); - var fix1 = new QuickFix("fix1", new[] { edit1, edit2 }); - var fix2 = new QuickFix("fix2", new[] { edit3 }); + var fix1 = new TextBasedQuickFix("fix1", [edit1, edit2]); + var fix2 = new TextBasedQuickFix("fix2", [edit3]); var issue = CreateIssue(fix1, fix2); var result = testSubject.Convert(issue, textSnapshotMock); @@ -119,19 +133,52 @@ public void Convert_IssueHasQuickFixes_QuickFixesSpansAreCalculated() result.QuickFixes.Should().NotBeEmpty(); result.QuickFixes.Count.Should().Be(2); - result.QuickFixes[0].Fix.Should().Be(fix1); - result.QuickFixes[0].EditVisualizations.Should().NotBeEmpty(); - result.QuickFixes[0].EditVisualizations.Count.Should().Be(2); - result.QuickFixes[0].EditVisualizations[0].Edit.Should().Be(edit1); - result.QuickFixes[0].EditVisualizations[0].Span.Should().Be(span1); - result.QuickFixes[0].EditVisualizations[1].Edit.Should().Be(edit2); - result.QuickFixes[0].EditVisualizations[1].Span.Should().Be(span2); - - result.QuickFixes[1].Fix.Should().Be(fix2); - result.QuickFixes[1].EditVisualizations.Should().NotBeEmpty(); - result.QuickFixes[1].EditVisualizations.Count.Should().Be(1); - result.QuickFixes[1].EditVisualizations[0].Edit.Should().Be(edit3); - result.QuickFixes[1].EditVisualizations[0].Span.Should().Be(span3); + var quickFix1 = result.QuickFixes[0].Should().BeOfType().Subject.QuickFixVisualization; + quickFix1.Fix.Should().Be(fix1); + quickFix1.EditVisualizations.Should().NotBeEmpty(); + quickFix1.EditVisualizations.Count.Should().Be(2); + quickFix1.EditVisualizations[0].Edit.Should().Be(edit1); + quickFix1.EditVisualizations[0].Span.Should().Be(span1); + quickFix1.EditVisualizations[1].Edit.Should().Be(edit2); + quickFix1.EditVisualizations[1].Span.Should().Be(span2); + + var quickFix2 = result.QuickFixes[1].Should().BeOfType().Subject.QuickFixVisualization; + quickFix2.Fix.Should().Be(fix2); + quickFix2.EditVisualizations.Should().NotBeEmpty(); + quickFix2.EditVisualizations.Count.Should().Be(1); + quickFix2.EditVisualizations[0].Edit.Should().Be(edit3); + quickFix2.EditVisualizations[0].Span.Should().Be(span3); + } + + [TestMethod] + public void Convert_IssueHasRoslynQuickFix_RoslynQuickFixIsFound_AddsQuickFixToResult() + { + var roslynQuickFix = new RoslynQuickFix(Guid.NewGuid()); + var issue = CreateIssue(roslynQuickFix); + var expectedQuickFixApplication = Substitute.For(); + roslynQuickFixProvider.TryGet(roslynQuickFix.Id, out Arg.Any()) + .Returns(x => + { + x[1] = expectedQuickFixApplication; + return true; + }); + + var result = testSubject.Convert(issue, textSnapshotMock); + + result.QuickFixes.Should().BeEquivalentTo(expectedQuickFixApplication); + } + + [TestMethod] + public void Convert_IssueHasRoslynQuickFix_RoslynQuickFixNotFound_ReturnsIssueWithoutQuickFix() + { + var roslynQuickFix = new RoslynQuickFix(Guid.NewGuid()); + var issue = CreateIssue(roslynQuickFix); + roslynQuickFixProvider.TryGet(roslynQuickFix.Id, out Arg.Any()) + .Returns(false); + + var result = testSubject.Convert(issue, textSnapshotMock); + + result.QuickFixes.Should().BeEmpty(); } [TestMethod] @@ -156,10 +203,10 @@ public void Convert_IssueHasNoFlows_IssueVisualizationWithoutFlows() SetupSpanCalculator(issue.PrimaryLocation.TextRange, issueSpan); var expectedIssueVisualization = new AnalysisIssueVisualization( - Array.Empty(), + [], issue, issueSpan, - Array.Empty()); + []); var actualIssueVisualization = testSubject.Convert(issue, textSnapshotMock); @@ -177,13 +224,13 @@ public void Convert_IssueHasOneFlow_IssueVisualizationWithFlow() SetupSpanCalculator(issue.PrimaryLocation.TextRange, issueSpan); var expectedLocationVisualization = new AnalysisIssueLocationVisualization(1, location); - var expectedFlowVisualization = new AnalysisIssueFlowVisualization(1, new[] { expectedLocationVisualization }, flow); + var expectedFlowVisualization = new AnalysisIssueFlowVisualization(1, [expectedLocationVisualization], flow); var expectedIssueVisualization = new AnalysisIssueVisualization( - new[] { expectedFlowVisualization }, + [expectedFlowVisualization], issue, issueSpan, - Array.Empty()); + []); var actualIssueVisualization = testSubject.Convert(issue, textSnapshotMock); @@ -207,18 +254,18 @@ public void Convert_IssueHasTwoFlows_IssueVisualizationWithTwoFlows() var expectedFirstFlowFirstLocationVisualization = new AnalysisIssueLocationVisualization(1, firstFlowFirstLocation); var expectedFirstFlowSecondLocationVisualization = new AnalysisIssueLocationVisualization(2, firstFlowSecondLocation); - var expectedFirstFlowVisualization = new AnalysisIssueFlowVisualization(1, new[] { expectedFirstFlowFirstLocationVisualization, expectedFirstFlowSecondLocationVisualization }, firstFlow); + var expectedFirstFlowVisualization = new AnalysisIssueFlowVisualization(1, [expectedFirstFlowFirstLocationVisualization, expectedFirstFlowSecondLocationVisualization], firstFlow); var expectedSecondFlowFirstLocationVisualization = new AnalysisIssueLocationVisualization(1, secondFlowFirstLocation); var expectedSecondFlowSecondLocationVisualization = new AnalysisIssueLocationVisualization(2, secondFlowSecondLocation); var expectedSecondFlowVisualization - = new AnalysisIssueFlowVisualization(2, new[] { expectedSecondFlowFirstLocationVisualization, expectedSecondFlowSecondLocationVisualization }, secondFlow); + = new AnalysisIssueFlowVisualization(2, [expectedSecondFlowFirstLocationVisualization, expectedSecondFlowSecondLocationVisualization], secondFlow); var expectedIssueVisualization = new AnalysisIssueVisualization( - new[] { expectedFirstFlowVisualization, expectedSecondFlowVisualization }, + [expectedFirstFlowVisualization, expectedSecondFlowVisualization], issue, issueSpan, - Array.Empty()); + []); var actualIssueVisualization = testSubject.Convert(issue, textSnapshotMock); @@ -243,13 +290,13 @@ public void Convert_IssueHasLocationsInDifferentFiles_CalculatesSpanForLocations var expectedLocation1 = new AnalysisIssueLocationVisualization(1, locationInSameFile) { Span = locationSpan }; var expectedLocation2 = new AnalysisIssueLocationVisualization(2, locationInAnotherFile) { Span = null }; - var expectedFlow = new AnalysisIssueFlowVisualization(1, new[] { expectedLocation1, expectedLocation2 }, flow); + var expectedFlow = new AnalysisIssueFlowVisualization(1, [expectedLocation1, expectedLocation2], flow); var expectedIssueVisualization = new AnalysisIssueVisualization( - new[] { expectedFlow }, + [expectedFlow], issue, issueSpan, - Array.Empty()); + []); var actualIssueVisualization = testSubject.Convert(issue, textSnapshotMock); @@ -276,7 +323,7 @@ private void AssertConversion(IAnalysisIssueVisualization expectedIssueVisualiza actualIssueVisualization.Flows.Should().BeEquivalentTo(expectedIssueVisualization.Flows, c => c.WithStrictOrdering()); } - private IAnalysisIssue CreateIssue(params IQuickFix[] quickFixes) + private IAnalysisIssue CreateIssue(params IQuickFixBase[] quickFixes) { var issue = new AnalysisIssue( Guid.NewGuid(), diff --git a/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixActionsSourceProviderTests.cs b/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixActionsSourceProviderTests.cs index 281445bada..7598458612 100644 --- a/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixActionsSourceProviderTests.cs +++ b/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixActionsSourceProviderTests.cs @@ -43,6 +43,7 @@ public void MefCtor_CheckIsExported() MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); } @@ -103,6 +104,7 @@ private static QuickFixActionsSourceProvider CreateTestSubject(ITextBuffer buffe bufferTagAggregatorFactoryService.Object, lightBulbBroker, Mock.Of(), + Substitute.For(), Mock.Of(), new NoOpThreadHandler()); } diff --git a/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixActionsSourceTests.cs b/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixActionsSourceTests.cs index 2d35cac047..365e5c623a 100644 --- a/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixActionsSourceTests.cs +++ b/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixActionsSourceTests.cs @@ -362,11 +362,12 @@ private QuickFixActionsSource CreateTestSubject( textView, textBuffer, Mock.Of(), + Substitute.For(), logger, threadHandling); } - private IAnalysisIssueVisualization CreateIssueViz(params IQuickFixVisualization[] fixes) + private IAnalysisIssueVisualization CreateIssueViz(params IQuickFixApplication[] fixes) { var issueViz = new Mock(); issueViz.Setup(x => x.QuickFixes).Returns(fixes); @@ -387,13 +388,13 @@ private Mock> CreateTagAggregatorForIssues(IAn return issueLocationsTagAggregator; } - private IQuickFixVisualization CreateQuickFixViz(bool canBeApplied, string message = null) + private IQuickFixApplication CreateQuickFixViz(bool canBeApplied, string message = null) { - var quickFixViz = new Mock(); - quickFixViz.Setup(x => x.CanBeApplied(textBuffer.CurrentSnapshot)).Returns(canBeApplied); - quickFixViz.Setup(x => x.Fix.Message).Returns(message); + var quickFixApplication = Substitute.For(); + quickFixApplication.Message.Returns(message); + quickFixApplication.CanBeApplied(textBuffer.CurrentSnapshot).Returns(canBeApplied); - return quickFixViz.Object; + return quickFixApplication; } private static Mock> CreateThrowingAggregator(Exception ex) diff --git a/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixSuggestedActionTests.cs b/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixSuggestedActionTests.cs index 2665025ee8..b732b0c9ae 100644 --- a/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixSuggestedActionTests.cs +++ b/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixSuggestedActionTests.cs @@ -18,296 +18,173 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using System.Windows; using Microsoft.VisualStudio.Text; -using Moq; +using NSubstitute.ExceptionExtensions; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Telemetry; -using SonarLint.VisualStudio.Infrastructure.VS; using SonarLint.VisualStudio.IssueVisualization.Editor.QuickActions.QuickFixes; using SonarLint.VisualStudio.IssueVisualization.Models; +using SonarLint.VisualStudio.TestInfrastructure; namespace SonarLint.VisualStudio.IssueVisualization.UnitTests.Editor.QuickActions.QuickFixes { [TestClass] public class QuickFixSuggestedActionTests { - [TestMethod] - public void DisplayName_ReturnsFixMessage() + private IQuickFixApplication quickFixApplication; + private ITextBuffer textBuffer; + private IAnalysisIssueVisualization issueViz; + private IQuickFixesTelemetryManager telemetryManager; + private TestLogger logger; + private NoOpThreadHandler threadHandling; + private ITextSnapshot snapshot; + private QuickFixSuggestedAction testSubject; + private const string RuleId = "test-rule-id"; + private SnapshotSpan originalSpan; + private IMessageBox messageBox; + + [TestInitialize] + public void TestInitialize() { - var quickFixViz = new Mock(); - quickFixViz.Setup(x => x.Fix.Message).Returns("some fix"); - - var testSubject = CreateTestSubject(quickFixViz.Object); - - testSubject.DisplayText.Should().Be(Resources.ProductNameCommandPrefix + "some fix"); + quickFixApplication = Substitute.For(); + snapshot = CreateTextSnapshot(); + textBuffer = CreateTextBuffer(snapshot); + issueViz = Substitute.For(); + issueViz.RuleId.Returns(RuleId); + messageBox = Substitute.For(); + + // Set up a non-empty span + originalSpan = new SnapshotSpan(snapshot, new Span(0, 10)); + issueViz.Span.Returns(originalSpan); + + telemetryManager = Substitute.For(); + logger = Substitute.ForPartsOf(); + threadHandling = Substitute.ForPartsOf(); + + testSubject = new QuickFixSuggestedAction( + quickFixApplication, + textBuffer, + issueViz, + telemetryManager, + messageBox, + logger, + threadHandling); } [TestMethod] - public void Invoke_QuickFixCanBeApplied_TelemetryIsSent() - { - var snapshot = CreateTextSnapshot(); - var quickFixViz = CreateQuickFixViz(snapshot.Object); - var textBuffer = CreateTextBuffer(snapshot.Object); - - var issueViz = new Mock(); - issueViz.Setup(x => x.RuleId).Returns("some rule"); - - var telemetryManager = new Mock(); + public void Ctor_SetsLogContext() => logger.Received(1).ForContext(Resources.QuickFixSuggestedAction_LogContext); - var testSubject = CreateTestSubject(quickFixViz.Object, - textBuffer.Object, - issueViz: issueViz.Object, - telemetryManager: telemetryManager.Object); - - testSubject.Invoke(CancellationToken.None); + [TestMethod] + public void DisplayName_ReturnsFixMessage() + { + const string message = "some fix"; + quickFixApplication.Message.Returns(message); - telemetryManager.Verify(x => x.QuickFixApplied("some rule"), Times.Once); - telemetryManager.VerifyNoOtherCalls(); + testSubject.DisplayText.Should().Be(Resources.ProductNameCommandPrefix + message); } [TestMethod] - public void Invoke_QuickFixCannotBeApplied_TelemetryNotSent() + public void Invoke_AppliesFixOnUiThreadWithTelemetry() { - var snapshot = CreateTextSnapshot(); - var quickFixViz = CreateNonApplicableQuickFixViz(snapshot.Object); - var textBuffer = CreateTextBuffer(snapshot.Object); + ConfigureQuickFixApplicationCanBeApplied(true, true); - var telemetryManager = new Mock(); - - var testSubject = CreateTestSubject(quickFixViz.Object, textBuffer.Object, telemetryManager: telemetryManager.Object); testSubject.Invoke(CancellationToken.None); - telemetryManager.Verify(x => x.QuickFixApplied(It.IsAny()), Times.Never); - telemetryManager.VerifyNoOtherCalls(); + Received.InOrder(() => + { + quickFixApplication.CanBeApplied(snapshot); + threadHandling.Run(Arg.Any>>()); + threadHandling.SwitchToMainThreadAsync(); + quickFixApplication.ApplyAsync(snapshot, Arg.Any()); + telemetryManager.QuickFixApplied(RuleId); + }); + issueViz.Received(1).Span = Arg.Is(s => s.IsEmpty); + issueViz.DidNotReceive().Span = originalSpan; } [TestMethod] - public void Invoke_AppliesFixWithOneEdit() + public void Invoke_QuickFixApplicationReturnsFalse_SpanIsRestored_TelemetryNotSent() { - var snapshot = CreateTextSnapshot(); - - var span = new Span(1, 10); - var editVisualization = CreateEditVisualization(new SnapshotSpan(snapshot.Object, span)); - var quickFixViz = CreateQuickFixViz(snapshot.Object, editVisualization.Object); - var textEdit = new Mock(MockBehavior.Strict); - var textBuffer = CreateTextBuffer(snapshot.Object, textEdit.Object); - - var sequence = new MockSequence(); + ConfigureQuickFixApplicationCanBeApplied(true, false); - textBuffer.InSequence(sequence).Setup(t => t.CreateEdit()).Returns(textEdit.Object); - textEdit.InSequence(sequence).Setup(t => t.Replace(span, "edit")).Returns(true); - textEdit.InSequence(sequence).Setup(t => t.Apply()).Returns(Mock.Of()); - - var testSubject = CreateTestSubject(quickFixViz.Object, textBuffer.Object); testSubject.Invoke(CancellationToken.None); - textBuffer.Verify(tb => tb.CreateEdit(), Times.Once(), "CreateEdit should be called once"); - textEdit.Verify(tb => tb.Replace(It.IsAny(), It.IsAny()), Times.Exactly(1), "Replace should be called one time"); - textEdit.Verify(tb => tb.Apply(), Times.Once(), "Apply should be called once"); + VerifyDidNotApply(); } [TestMethod] - public void Invoke_AppliesFixWithMultipleEdits() + public void Invoke_QuickFixApplicationThrowsException_SpanIsRestored_TelemetryNotSent() { - var snapshot = CreateTextSnapshot(); - - var span1 = new Span(1, 10); - var editVisualization1 = CreateEditVisualization(new SnapshotSpan(snapshot.Object, span1), "edit1"); - - var span2 = new Span(2, 20); - var editVisualization2 = CreateEditVisualization(new SnapshotSpan(snapshot.Object, span2), "edit2"); - - var span3 = new Span(3, 30); - var editVisualization3 = CreateEditVisualization(new SnapshotSpan(snapshot.Object, span3), "edit3"); - - var quickFixViz = CreateQuickFixViz(snapshot.Object, - editVisualization1.Object, - editVisualization2.Object, - editVisualization3.Object); + quickFixApplication.CanBeApplied(snapshot).Returns(true); + var exception = new Exception("test"); + quickFixApplication.ApplyAsync(snapshot, Arg.Any()).ThrowsAsync(exception); - var textEdit = new Mock(MockBehavior.Strict); - var textBuffer = CreateTextBuffer(snapshot.Object, textEdit.Object); - - var sequence = new MockSequence(); - - textBuffer.InSequence(sequence).Setup(t => t.CreateEdit()).Returns(textEdit.Object); - textEdit.InSequence(sequence).Setup(t => t.Replace(span1, "edit1")).Returns(true); - textEdit.InSequence(sequence).Setup(t => t.Replace(span2, "edit2")).Returns(true); - textEdit.InSequence(sequence).Setup(t => t.Replace(span3, "edit3")).Returns(true); - textEdit.InSequence(sequence).Setup(t => t.Apply()).Returns(Mock.Of()); - - var testSubject = CreateTestSubject(quickFixViz.Object, textBuffer.Object); - testSubject.Invoke(CancellationToken.None); + var act = () => testSubject.Invoke(CancellationToken.None); - textBuffer.Verify(tb => tb.CreateEdit(), Times.Once(), "CreateEdit should be called once"); - textEdit.Verify(tb => tb.Replace(It.IsAny(), It.IsAny()), Times.Exactly(3), "Replace should be called three time"); - textEdit.Verify(tb => tb.Apply(), Times.Once(), "Apply should be called once"); + act.Should().Throw().Which.Should().Be(exception); + VerifyDidNotApply(); } [TestMethod] public void Invoke_CancellationTokenIsCancelled_NoChanges() { - var quickFixViz = new Mock(); - var textBuffer = new Mock(); - var issueViz = new Mock(); - - var testSubject = CreateTestSubject(quickFixViz.Object, textBuffer.Object, issueViz: issueViz.Object); - testSubject.Invoke(new CancellationToken(canceled: true)); - quickFixViz.VerifyNoOtherCalls(); - textBuffer.VerifyNoOtherCalls(); - issueViz.VerifyNoOtherCalls(); + quickFixApplication.DidNotReceiveWithAnyArgs().ApplyAsync(default, default); + issueViz.DidNotReceiveWithAnyArgs().Span = Arg.Any(); + telemetryManager.DidNotReceiveWithAnyArgs().QuickFixApplied(Arg.Any()); } [TestMethod] public void Invoke_QuickFixIsNotApplicable_NoChanges() { - var snapshot = Mock.Of(); - var quickFixViz = CreateNonApplicableQuickFixViz(snapshot); - var issueViz = new Mock(); - var textBuffer = CreateTextBuffer(snapshot); - - var testSubject = CreateTestSubject(quickFixViz.Object, textBuffer.Object, issueViz: issueViz.Object); + ConfigureQuickFixApplicationCanBeApplied(false, false); testSubject.Invoke(CancellationToken.None); - quickFixViz.Verify(x => x.CanBeApplied(snapshot), Times.Once); - quickFixViz.VerifyNoOtherCalls(); - - textBuffer.VerifyGet(x => x.CurrentSnapshot, Times.Once); - textBuffer.VerifyNoOtherCalls(); - - issueViz.VerifyGet(x => x.RuleId, Times.Once); - issueViz.VerifyNoOtherCalls(); + quickFixApplication.Received(1).CanBeApplied(snapshot); + quickFixApplication.DidNotReceiveWithAnyArgs().ApplyAsync(default, default); + issueViz.DidNotReceiveWithAnyArgs().Span = Arg.Any(); + telemetryManager.DidNotReceiveWithAnyArgs().QuickFixApplied(Arg.Any()); } - [TestMethod] - public void Invoke_SpansAreTranslatedCorrectly() + private void VerifyDidNotApply() { - var snapshot = CreateTextSnapshot(); - var span1 = new Span(1, 10); - var span2 = new Span(20, 30); - - var originalSnapshotSpan = new SnapshotSpan(snapshot.Object, span1); - var modifiedSnapshotSpan = new SnapshotSpan(snapshot.Object, span2); - - var spanTranslator = new Mock(); - spanTranslator - .Setup(x => x.TranslateTo(originalSnapshotSpan, snapshot.Object, SpanTrackingMode.EdgeExclusive)) - .Returns(modifiedSnapshotSpan); - - var editVisualization = CreateEditVisualization(originalSnapshotSpan, text: "some edit"); - var quickFixViz = CreateQuickFixViz(snapshot.Object, editVisualization.Object); - - var textEdit = new Mock(); - var textBuffer = CreateTextBuffer(snapshot.Object, textEdit.Object); - - var testSubject = CreateTestSubject(quickFixViz.Object, textBuffer.Object, spanTranslator.Object); - testSubject.Invoke(CancellationToken.None); - - textEdit.Verify(t => t.Replace(span2, "some edit"), Times.Once); - } - - [TestMethod] - public void Invoke_SpanInvalidatedCorrectly() - { - var snapshot = CreateTextSnapshot(); - var editVisualization = CreateEditVisualization(new SnapshotSpan(snapshot.Object, new Span(1, 10))); - var quickFixViz = CreateQuickFixViz(snapshot.Object, editVisualization.Object); - var textBuffer = CreateTextBuffer(snapshot.Object); - - var issueViz = new Mock(); - - var testSubject = CreateTestSubject(quickFixViz.Object, textBuffer.Object, issueViz: issueViz.Object); - testSubject.Invoke(CancellationToken.None); - - issueViz.VerifySet(iv => iv.Span = new SnapshotSpan()); - } - - private static Mock CreateTextSnapshot() - { - var snapShot = new Mock(); - snapShot.SetupGet(ss => ss.Length).Returns(int.MaxValue); - - return snapShot; - } - - private static QuickFixSuggestedAction CreateTestSubject( - IQuickFixVisualization quickFixViz, - ITextBuffer textBuffer = null, - ISpanTranslator spanTranslator = null, - IAnalysisIssueVisualization issueViz = null, - IQuickFixesTelemetryManager telemetryManager = null) - { - if (spanTranslator == null) + var didNotApplyMessage = string.Format(Resources.QuickFixSuggestedAction_CouldNotApply, RuleId); + Received.InOrder(() => { - SnapshotSpan originalSnapshotSpan = new(); - - var doNothingSpanTranslator = new Mock(); - doNothingSpanTranslator.Setup(x => x.TranslateTo( - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Callback((SnapshotSpan snapshotSpan, ITextSnapshot textSnapshot, SpanTrackingMode mode) - => originalSnapshotSpan = snapshotSpan) - .Returns(() => originalSnapshotSpan); - - spanTranslator = doNothingSpanTranslator.Object; - } - - issueViz ??= Mock.Of(); - telemetryManager ??= Mock.Of(); - - return new QuickFixSuggestedAction(quickFixViz, - textBuffer, - issueViz, - telemetryManager, - Mock.Of(), - spanTranslator); + quickFixApplication.CanBeApplied(snapshot); + threadHandling.Run(Arg.Any>>()); + threadHandling.SwitchToMainThreadAsync(); + quickFixApplication.ApplyAsync(snapshot, Arg.Any()); + messageBox.Show(didNotApplyMessage, Resources.QuickFixSuggestedAction_CouldNotApplyMessageBoxCaption, MessageBoxButton.OK, MessageBoxImage.Error); + }); + issueViz.Received().Span = originalSpan; + telemetryManager.DidNotReceiveWithAnyArgs().QuickFixApplied(Arg.Any()); + logger.AssertPartialOutputStringExists(didNotApplyMessage); } - private static Mock CreateQuickFixViz(ITextSnapshot snapShot, params IQuickFixEditVisualization[] editVisualizations) => - CreateQuickFixViz(snapShot, true, editVisualizations); - - private static Mock CreateNonApplicableQuickFixViz(ITextSnapshot snapShot) => CreateQuickFixViz(snapShot, false); - - private static Mock CreateQuickFixViz( - ITextSnapshot snapShot, - bool canBeApplied = true, - params IQuickFixEditVisualization[] editVisualizations) + private static ITextSnapshot CreateTextSnapshot() { - var quickFixViz = new Mock(); - - quickFixViz - .Setup(x => x.EditVisualizations) - .Returns(editVisualizations); - - quickFixViz - .Setup(x => x.CanBeApplied(snapShot)) - .Returns(canBeApplied); - - return quickFixViz; + var snapshot = Substitute.For(); + snapshot.Length.Returns(int.MaxValue); + return snapshot; } - private static Mock CreateEditVisualization(SnapshotSpan snapshotSpan, string text = "edit") + private void ConfigureQuickFixApplicationCanBeApplied(bool canBeApplied, bool willBeApplied) { - var editVisualization = new Mock(); - - editVisualization.Setup(e => e.Edit.NewText).Returns(text); - editVisualization.Setup(e => e.Span).Returns(snapshotSpan); + quickFixApplication.CanBeApplied(snapshot) + .Returns(canBeApplied); - return editVisualization; + quickFixApplication.ApplyAsync(snapshot, Arg.Any()) + .Returns(willBeApplied); } - private static Mock CreateTextBuffer(ITextSnapshot snapShot, ITextEdit textEdit = null) + private static ITextBuffer CreateTextBuffer(ITextSnapshot snapShot) { - textEdit ??= Mock.Of(); - - var textBuffer = new Mock(MockBehavior.Strict); - textBuffer.Setup(x => x.CurrentSnapshot).Returns(snapShot); - textBuffer.Setup(t => t.CreateEdit()).Returns(textEdit); - + var textBuffer = Substitute.For(); + textBuffer.CurrentSnapshot.Returns(snapShot); return textBuffer; } } diff --git a/src/IssueViz.UnitTests/Models/QuickFixVisualizationTests.cs b/src/IssueViz.UnitTests/Models/QuickFixVisualizationTests.cs index 84e157923b..27aaa9f71a 100644 --- a/src/IssueViz.UnitTests/Models/QuickFixVisualizationTests.cs +++ b/src/IssueViz.UnitTests/Models/QuickFixVisualizationTests.cs @@ -95,13 +95,13 @@ public void CanBeApplied_SameTextButDifferentCasing_False() testSubject.CanBeApplied(snapshot.Object).Should().BeFalse(); } - private IQuickFixEditVisualization SetupEditVisualization(Mock snapshot, + private ITextBasedQuickFixEditVisualization SetupEditVisualization(Mock snapshot, Mock spanTranslator, string originalText, string updatedText) => SetupEditVisualization(snapshot, spanTranslator, new Span(1, 2), originalText, updatedText); - private IQuickFixEditVisualization SetupEditVisualization(Mock snapshot, + private ITextBasedQuickFixEditVisualization SetupEditVisualization(Mock snapshot, Mock spanTranslator, Span originalSpan, bool isSameText) @@ -112,7 +112,7 @@ private IQuickFixEditVisualization SetupEditVisualization(Mock sn return SetupEditVisualization(snapshot, spanTranslator, originalSpan, originalText, updatedText); } - private IQuickFixEditVisualization SetupEditVisualization(Mock snapshot, + private ITextBasedQuickFixEditVisualization SetupEditVisualization(Mock snapshot, Mock spanTranslator, Span originalSpan, string originalText, @@ -120,7 +120,7 @@ private IQuickFixEditVisualization SetupEditVisualization(Mock sn { var originalSnapshotSpan = new SnapshotSpan(snapshot.Object, originalSpan); - var editViz = new Mock(); + var editViz = new Mock(); editViz.Setup(x => x.Span).Returns(originalSnapshotSpan); var updatedSnapshotSpan = new SnapshotSpan(snapshot.Object, new Span(originalSpan.Start + 1, originalSpan.Length)); @@ -143,7 +143,7 @@ private static Mock CreateSnapshot() return snapshot; } - private IQuickFixVisualization CreateTestSubject(ISpanTranslator spanTranslator, params IQuickFixEditVisualization[] editVisualizations) => - new QuickFixVisualization(Mock.Of(), editVisualizations, spanTranslator); + private ITextBasedQuickFixVisualization CreateTestSubject(ISpanTranslator spanTranslator, params ITextBasedQuickFixEditVisualization[] editVisualizations) => + new TextBasedQuickFixVisualization(Mock.Of(), editVisualizations, spanTranslator); } } diff --git a/src/IssueViz.UnitTests/Models/TextBasedQuickFixApplicationTests.cs b/src/IssueViz.UnitTests/Models/TextBasedQuickFixApplicationTests.cs new file mode 100644 index 0000000000..51f5f7f498 --- /dev/null +++ b/src/IssueViz.UnitTests/Models/TextBasedQuickFixApplicationTests.cs @@ -0,0 +1,135 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.VisualStudio.Text; +using SonarLint.VisualStudio.Core.Analysis; +using SonarLint.VisualStudio.Infrastructure.VS; +using SonarLint.VisualStudio.IssueVisualization.Models; + +namespace SonarLint.VisualStudio.IssueVisualization.UnitTests.Models; + +[TestClass] +public class TextBasedQuickFixApplicationTests +{ + private ITextSnapshot snapshot; + private ITextBasedQuickFixVisualization quickFixVisualization; + private ISpanTranslator spanTranslator; + private ITextBuffer textBuffer; + private ITextEdit textEdit; + private TextBasedQuickFixApplication testSubject; + + [TestInitialize] + public void TestInitialize() + { + snapshot = Substitute.For(); + snapshot.Length.Returns(int.MaxValue); + quickFixVisualization = Substitute.For(); + spanTranslator = Substitute.For(); + + testSubject = new TextBasedQuickFixApplication(quickFixVisualization, spanTranslator); + + SetupTextBufferAndEdit(); + } + + [TestMethod] + public void Message_ReturnsFixMessage() + { + const string expectedMessage = "test message"; + var quickFix = Substitute.For(); + quickFix.Message.Returns(expectedMessage); + quickFixVisualization.Fix.Returns(quickFix); + + testSubject.Message.Should().Be(expectedMessage); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void CanBeApplied_DelegatesToVisualization(bool expectedResult) + { + quickFixVisualization.CanBeApplied(snapshot).Returns(expectedResult); + + testSubject.CanBeApplied(snapshot).Should().Be(expectedResult); + quickFixVisualization.Received(1).CanBeApplied(snapshot); + } + + [TestMethod] + public async Task ApplyAsync_CreatesEditAndAppliesAllChanges() + { + var span1 = new SnapshotSpan(snapshot, new Span(1, 10)); + var span2 = new SnapshotSpan(snapshot, new Span(20, 5)); + var translatedSpan1 = new SnapshotSpan(snapshot, new Span(2, 10)); + var translatedSpan2 = new SnapshotSpan(snapshot, new Span(25, 5)); + SetupEditVisualizations( + (span1, "new text 1", translatedSpan1), + (span2, "new text 2", translatedSpan2)); + + await testSubject.ApplyAsync(snapshot, CancellationToken.None); + + textBuffer.Received(1).CreateEdit(); + spanTranslator.Received(1).TranslateTo(span1, snapshot, SpanTrackingMode.EdgeExclusive); + spanTranslator.Received(1).TranslateTo(span2, snapshot, SpanTrackingMode.EdgeExclusive); + textEdit.Received(1).Replace(translatedSpan1.Span, "new text 1"); + textEdit.Received(1).Replace(translatedSpan2.Span, "new text 2"); + textEdit.Received(1).Apply(); + } + + [TestMethod] + public async Task ApplyAsync_CancellationRequested_DoesNotApplyChanges() + { + var cancellationToken = new CancellationToken(true); + var span = new SnapshotSpan(snapshot, new Span(1, 10)); + SetupEditVisualizations((span, "new text", span)); + + var act = () => testSubject.ApplyAsync(snapshot, cancellationToken); + await act.Should().ThrowAsync(); + + textBuffer.Received(1).CreateEdit(); + textEdit.DidNotReceiveWithAnyArgs().Apply(); + } + + private void SetupTextBufferAndEdit() + { + textEdit = Substitute.For(); + textBuffer = Substitute.For(); + textBuffer.CreateEdit().Returns(textEdit); + textBuffer.CurrentSnapshot.Returns(snapshot); + snapshot.TextBuffer.Returns(textBuffer); + } + + private void SetupEditVisualizations(params (SnapshotSpan span, string newText, SnapshotSpan translatedSpan)[] edits) + { + var editVisualizations = new ITextBasedQuickFixEditVisualization[edits.Length]; + + for (int i = 0; i < edits.Length; i++) + { + var edit = Substitute.For(); + edit.Span.Returns(edits[i].span); + edit.Edit.NewText.Returns(edits[i].newText); + editVisualizations[i] = edit; + + spanTranslator + .TranslateTo(edits[i].span, snapshot, SpanTrackingMode.EdgeExclusive) + .Returns(edits[i].translatedSpan); + } + + quickFixVisualization.EditVisualizations.Returns(editVisualizations); + } +} diff --git a/src/IssueViz.UnitTests/packages.lock.json b/src/IssueViz.UnitTests/packages.lock.json index 33a58cfe9b..3e3440eb3a 100644 --- a/src/IssueViz.UnitTests/packages.lock.json +++ b/src/IssueViz.UnitTests/packages.lock.json @@ -111,16 +111,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -1308,8 +1298,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixActionsSource.cs b/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixActionsSource.cs index ed79ea9b4f..903471f81d 100644 --- a/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixActionsSource.cs +++ b/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixActionsSource.cs @@ -38,6 +38,7 @@ internal sealed class QuickFixActionsSource : ISuggestedActionsSource private readonly ILogger logger; private readonly IThreadHandling threadHandling; private readonly ITagAggregator issueLocationsTagAggregator; + private readonly IMessageBox messageBox; private readonly IQuickFixesTelemetryManager quickFixesTelemetryManager; public QuickFixActionsSource(ILightBulbBroker lightBulbBroker, @@ -45,6 +46,7 @@ public QuickFixActionsSource(ILightBulbBroker lightBulbBroker, ITextView textView, ITextBuffer textBuffer, IQuickFixesTelemetryManager quickFixesTelemetryManager, + IMessageBox messageBox, ILogger logger, IThreadHandling threadHandling) { @@ -52,6 +54,7 @@ public QuickFixActionsSource(ILightBulbBroker lightBulbBroker, this.textBuffer = textBuffer; this.textView = textView; this.quickFixesTelemetryManager = quickFixesTelemetryManager; + this.messageBox = messageBox; this.logger = logger; this.threadHandling = threadHandling; @@ -76,7 +79,7 @@ public IEnumerable GetSuggestedActions( { var applicableFixes = issueViz.QuickFixes.Where(x => x.CanBeApplied(textBuffer.CurrentSnapshot)); - allActions.AddRange(applicableFixes.Select(fix => new QuickFixSuggestedAction(fix, textBuffer, issueViz, quickFixesTelemetryManager, logger))); + allActions.AddRange(applicableFixes.Select(fix => new QuickFixSuggestedAction(fix, textBuffer, issueViz, quickFixesTelemetryManager, messageBox, logger, threadHandling))); } } } diff --git a/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixActionsSourceProvider.cs b/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixActionsSourceProvider.cs index 1cee363d4d..f842413a9e 100644 --- a/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixActionsSourceProvider.cs +++ b/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixActionsSourceProvider.cs @@ -38,6 +38,7 @@ internal class QuickFixActionsSourceProvider( IBufferTagAggregatorFactoryService bufferTagAggregatorFactoryService, ILightBulbBroker lightBulbBroker, IQuickFixesTelemetryManager quickFixesTelemetryManager, + IMessageBox messageBox, ILogger logger, IThreadHandling threadHandling) : ISuggestedActionsSourceProvider @@ -62,6 +63,7 @@ public ISuggestedActionsSource CreateSuggestedActionsSource(ITextView textView, textView, textBuffer, quickFixesTelemetryManager, + messageBox, logger, threadHandling); } diff --git a/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixSuggestedAction.cs b/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixSuggestedAction.cs index 114f39fa3c..367356cd24 100644 --- a/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixSuggestedAction.cs +++ b/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixSuggestedAction.cs @@ -18,77 +18,86 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using System.Windows; using Microsoft.VisualStudio.Text; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Telemetry; -using SonarLint.VisualStudio.Infrastructure.VS; using SonarLint.VisualStudio.IssueVisualization.Models; -namespace SonarLint.VisualStudio.IssueVisualization.Editor.QuickActions.QuickFixes +namespace SonarLint.VisualStudio.IssueVisualization.Editor.QuickActions.QuickFixes; + +internal class QuickFixSuggestedAction( + IQuickFixApplication quickFixApplication, + ITextBuffer textBuffer, + IAnalysisIssueVisualization issueViz, + IQuickFixesTelemetryManager quickFixesTelemetryManager, + IMessageBox messageBox, + ILogger logger, + IThreadHandling threadHandling) + : BaseSuggestedAction { - internal class QuickFixSuggestedAction : BaseSuggestedAction - { - private readonly IQuickFixVisualization quickFixVisualization; - private readonly ITextBuffer textBuffer; - private readonly ISpanTranslator spanTranslator; - private readonly IAnalysisIssueVisualization issueViz; - private readonly IQuickFixesTelemetryManager quickFixesTelemetryManager; - private readonly ILogger logger; + private readonly ILogger logger = logger.ForContext(Resources.QuickFixSuggestedAction_LogContext); + + public override string DisplayText => Resources.ProductNameCommandPrefix + quickFixApplication.Message; - public QuickFixSuggestedAction( - IQuickFixVisualization quickFixVisualization, - ITextBuffer textBuffer, - IAnalysisIssueVisualization issueViz, - IQuickFixesTelemetryManager quickFixesTelemetryManager, - ILogger logger) - : this(quickFixVisualization, textBuffer, issueViz, quickFixesTelemetryManager, logger, new SpanTranslator()) + public override void Invoke(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) { + return; } - internal QuickFixSuggestedAction( - IQuickFixVisualization quickFixVisualization, - ITextBuffer textBuffer, - IAnalysisIssueVisualization issueViz, - IQuickFixesTelemetryManager quickFixesTelemetryManager, - ILogger logger, - ISpanTranslator spanTranslator) + if (!quickFixApplication.CanBeApplied(textBuffer.CurrentSnapshot)) { - this.quickFixVisualization = quickFixVisualization; - this.textBuffer = textBuffer; - this.issueViz = issueViz; - this.quickFixesTelemetryManager = quickFixesTelemetryManager; - this.logger = logger; - this.spanTranslator = spanTranslator; + logger.LogVerbose("Quick fix cannot be applied as the text has changed. Issue: " + issueViz.RuleId); + return; } - public override string DisplayText => Resources.ProductNameCommandPrefix + quickFixVisualization.Fix.Message; - - public override void Invoke(CancellationToken cancellationToken) + threadHandling.Run(async () => { - if (cancellationToken.IsCancellationRequested) - { - return; - } + await threadHandling.SwitchToMainThreadAsync(); + + var isHandled = await HandleQuickFixAsync(cancellationToken); - if (!quickFixVisualization.CanBeApplied(textBuffer.CurrentSnapshot)) + if (isHandled) { - logger.LogVerbose("[Quick Fixes] Quick fix cannot be applied as the text has changed. Issue: " + issueViz.RuleId); - return; + quickFixesTelemetryManager.QuickFixApplied(issueViz.RuleId); } - var textEdit = textBuffer.CreateEdit(); + return 0; + }); + } - foreach (var edit in quickFixVisualization.EditVisualizations) - { - var updatedSpan = spanTranslator.TranslateTo(edit.Span, textBuffer.CurrentSnapshot, SpanTrackingMode.EdgeExclusive); + private async Task HandleQuickFixAsync(CancellationToken cancellationToken) + { + var originalSpan = issueViz.Span; + issueViz.InvalidateSpan(); + + var isApplied = false; - textEdit.Replace(updatedSpan, edit.Edit.NewText); + try + { + isApplied = await quickFixApplication.ApplyAsync(textBuffer.CurrentSnapshot, cancellationToken); + } + finally + { + if (!isApplied) + { + issueViz.Span = originalSpan; + NotifyUser(); } + } - issueViz.InvalidateSpan(); - textEdit.Apply(); + return isApplied; + } - quickFixesTelemetryManager.QuickFixApplied(issueViz.RuleId); - } + private void NotifyUser() + { + logger.WriteLine(Resources.QuickFixSuggestedAction_CouldNotApply, issueViz.RuleId); + messageBox.Show( + string.Format(Resources.QuickFixSuggestedAction_CouldNotApply,issueViz.RuleId), + Resources.QuickFixSuggestedAction_CouldNotApplyMessageBoxCaption, + MessageBoxButton.OK, + MessageBoxImage.Error); } } diff --git a/src/IssueViz/Models/AnalysisIssueVisualization.cs b/src/IssueViz/Models/AnalysisIssueVisualization.cs index 550a79c08a..b450f7bf3c 100644 --- a/src/IssueViz/Models/AnalysisIssueVisualization.cs +++ b/src/IssueViz/Models/AnalysisIssueVisualization.cs @@ -32,7 +32,7 @@ public interface IAnalysisIssueVisualization : IAnalysisIssueLocationVisualizati IAnalysisIssueBase Issue { get; } - IReadOnlyList QuickFixes { get; } + IReadOnlyList QuickFixes { get; } bool IsResolved { get; } } @@ -47,7 +47,7 @@ public AnalysisIssueVisualization( IReadOnlyList flows, IAnalysisIssueBase issue, SnapshotSpan? span, - IReadOnlyList quickFixes) + IReadOnlyList quickFixes) { Flows = flows; Issue = issue; @@ -57,7 +57,7 @@ public AnalysisIssueVisualization( } public IReadOnlyList Flows { get; } - public IReadOnlyList QuickFixes { get; } + public IReadOnlyList QuickFixes { get; } public IAnalysisIssueBase Issue { get; } public int StepNumber => 0; public IAnalysisIssueLocation Location => Issue.PrimaryLocation; diff --git a/src/IssueViz/Models/AnalysisIssueVisualizationConverter.cs b/src/IssueViz/Models/AnalysisIssueVisualizationConverter.cs index f3142dd009..ec2c8496a8 100644 --- a/src/IssueViz/Models/AnalysisIssueVisualizationConverter.cs +++ b/src/IssueViz/Models/AnalysisIssueVisualizationConverter.cs @@ -21,7 +21,9 @@ using System.ComponentModel.Composition; using Microsoft.VisualStudio.Text; using SonarLint.VisualStudio.Core.Analysis; +using SonarLint.VisualStudio.Core.CSharpVB; using SonarLint.VisualStudio.Core.Helpers; +using SonarLint.VisualStudio.Infrastructure.VS; using SonarLint.VisualStudio.IssueVisualization.Editor; namespace SonarLint.VisualStudio.IssueVisualization.Models @@ -33,18 +35,11 @@ public interface IAnalysisIssueVisualizationConverter [Export(typeof(IAnalysisIssueVisualizationConverter))] [PartCreationPolicy(CreationPolicy.Shared)] - internal class AnalysisIssueVisualizationConverter : IAnalysisIssueVisualizationConverter + [method: ImportingConstructor] + internal class AnalysisIssueVisualizationConverter(IIssueSpanCalculator issueSpanCalculator, ISpanTranslator spanTranslator, IRoslynQuickFixProvider roslynQuickFixProvider) : IAnalysisIssueVisualizationConverter { - private static readonly IReadOnlyList EmptyConvertedFlows = Array.Empty(); - private static readonly IReadOnlyList EmptyConvertedFixes = Array.Empty(); - - private readonly IIssueSpanCalculator issueSpanCalculator; - - [ImportingConstructor] - public AnalysisIssueVisualizationConverter(IIssueSpanCalculator issueSpanCalculator) - { - this.issueSpanCalculator = issueSpanCalculator; - } + private static readonly IReadOnlyList EmptyConvertedFlows = []; + private static readonly IReadOnlyList EmptyConvertedFixes = []; public IAnalysisIssueVisualization Convert(IAnalysisIssueBase issue, ITextSnapshot textSnapshot = null) { @@ -108,28 +103,48 @@ private IReadOnlyList Convert(IEnumerable GetQuickFixVisualizations(IAnalysisIssueBase issue, ITextSnapshot textSnapshot) + private IReadOnlyList GetQuickFixVisualizations(IAnalysisIssueBase issue, ITextSnapshot textSnapshot) { if (!(issue is IAnalysisIssue analysisIssue) || textSnapshot == null) { return EmptyConvertedFixes; } - var convertedQuickFixes = analysisIssue.Fixes.Select(fix => - { - var editVisualizations = fix.Edits.Select(edit => + return analysisIssue + .Fixes + .Select(fix => { - var editSpan = issueSpanCalculator.CalculateSpan(edit.RangeToReplace, textSnapshot) ?? new SnapshotSpan(); + if (fix is IRoslynQuickFix roslynQuickFix) + { + if (roslynQuickFixProvider.TryGet(roslynQuickFix.Id, out var roslynQuickFixApplication)) + { + return roslynQuickFixApplication; + } + Debug.Fail("Roslyn quick fix not found"); + } + if (fix is ITextBasedQuickFix textBasedQuickFix) + { + return HandleTextBasedQuickFix(textSnapshot, textBasedQuickFix); + } + return null; + }) + .Where(x => x is not null).ToArray(); + } - return new QuickFixEditVisualization(edit, editSpan); - }); + private TextBasedQuickFixApplication HandleTextBasedQuickFix( + ITextSnapshot textSnapshot, + ITextBasedQuickFix textBasedQuickFix) + { + var editVisualizations = textBasedQuickFix.Edits.Select(edit => + { + var editSpan = issueSpanCalculator.CalculateSpan(edit.RangeToReplace, textSnapshot) ?? new SnapshotSpan(); - var fixVisualization = new QuickFixVisualization(fix, editVisualizations.ToArray()); + return new TextBasedQuickFixEditVisualization(edit, editSpan); + }); - return fixVisualization; - }).ToArray(); + var fixVisualization = new TextBasedQuickFixVisualization(textBasedQuickFix, editVisualizations.ToArray(), spanTranslator); - return convertedQuickFixes; + return new TextBasedQuickFixApplication(fixVisualization, spanTranslator); } } } diff --git a/src/SonarQube.Client/Api/Common/ServerImpact.cs b/src/IssueViz/Models/IQuickFixApplication.cs similarity index 73% rename from src/SonarQube.Client/Api/Common/ServerImpact.cs rename to src/IssueViz/Models/IQuickFixApplication.cs index ce8d3af5c7..ecde522d68 100644 --- a/src/SonarQube.Client/Api/Common/ServerImpact.cs +++ b/src/IssueViz/Models/IQuickFixApplication.cs @@ -18,16 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using Newtonsoft.Json; +using Microsoft.VisualStudio.Text; -namespace SonarQube.Client.Api.Common -{ - internal class ServerImpact - { - [JsonProperty("softwareQuality")] - public string SoftwareQuality { get; set; } +namespace SonarLint.VisualStudio.IssueVisualization.Models; - [JsonProperty("severity")] - public string Severity { get; set; } - } +public interface IQuickFixApplication +{ + string Message { get; } + bool CanBeApplied(ITextSnapshot currentSnapshot); + Task ApplyAsync(ITextSnapshot currentSnapshot, CancellationToken cancellationToken); } diff --git a/src/SonarQube.Client/Api/IGetExclusionsRequest.cs b/src/IssueViz/Models/IRoslynQuickFixStorage.cs similarity index 79% rename from src/SonarQube.Client/Api/IGetExclusionsRequest.cs rename to src/IssueViz/Models/IRoslynQuickFixStorage.cs index 13a62385b8..6543ae2b70 100644 --- a/src/SonarQube.Client/Api/IGetExclusionsRequest.cs +++ b/src/IssueViz/Models/IRoslynQuickFixStorage.cs @@ -18,13 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using SonarQube.Client.Models; -using SonarQube.Client.Requests; +namespace SonarLint.VisualStudio.IssueVisualization.Models; -namespace SonarQube.Client.Api +public interface IRoslynQuickFixProvider { - public interface IGetExclusionsRequest : IRequest - { - string ProjectKey { get; set; } - } + bool TryGet(Guid id, out IQuickFixApplication roslynQuickFix); } diff --git a/src/IssueViz/Models/QuickFixVisualization.cs b/src/IssueViz/Models/ITextBasedQuickFixVisualization.cs similarity index 60% rename from src/IssueViz/Models/QuickFixVisualization.cs rename to src/IssueViz/Models/ITextBasedQuickFixVisualization.cs index eba3536bc8..1975c42f0f 100644 --- a/src/IssueViz/Models/QuickFixVisualization.cs +++ b/src/IssueViz/Models/ITextBasedQuickFixVisualization.cs @@ -18,20 +18,17 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.VisualStudio.Text; using SonarLint.VisualStudio.Core.Analysis; using SonarLint.VisualStudio.Infrastructure.VS; namespace SonarLint.VisualStudio.IssueVisualization.Models { - public interface IQuickFixVisualization + public interface ITextBasedQuickFixVisualization { - IQuickFix Fix { get; } + ITextBasedQuickFix Fix { get; } - IReadOnlyList EditVisualizations { get; } + IReadOnlyList EditVisualizations { get; } /// /// Returns false if the snapshot has been edited so the fix can no longer be applied, otherwise true. @@ -39,34 +36,20 @@ public interface IQuickFixVisualization bool CanBeApplied(ITextSnapshot currentSnapshot); } - internal class QuickFixVisualization : IQuickFixVisualization + internal class TextBasedQuickFixVisualization( + ITextBasedQuickFix fix, + IReadOnlyList editVisualizations, + ISpanTranslator spanTranslator) + : ITextBasedQuickFixVisualization { - private readonly ISpanTranslator spanTranslator; + public ITextBasedQuickFix Fix { get; } = fix; - public QuickFixVisualization(IQuickFix fix, IReadOnlyList editVisualizations) - : this(fix, editVisualizations, new SpanTranslator()) - { - Fix = fix; - EditVisualizations = editVisualizations; - } - - internal QuickFixVisualization(IQuickFix fix, - IReadOnlyList editVisualizations, - ISpanTranslator spanTranslator) - { - this.spanTranslator = spanTranslator; - Fix = fix; - EditVisualizations = editVisualizations; - } - - public IQuickFix Fix { get; } - - public IReadOnlyList EditVisualizations { get; } + public IReadOnlyList EditVisualizations { get; } = editVisualizations; public bool CanBeApplied(ITextSnapshot currentSnapshot) => EditVisualizations.All(x => IsTextUnchanged(x, currentSnapshot)); - private bool IsTextUnchanged(IQuickFixEditVisualization editViz, ITextSnapshot currentSnapshot) + private bool IsTextUnchanged(ITextBasedQuickFixEditVisualization editViz, ITextSnapshot currentSnapshot) { var originalText = editViz.Span.GetText(); var updatedSpan = spanTranslator.TranslateTo(editViz.Span, currentSnapshot, SpanTrackingMode.EdgeExclusive); @@ -76,16 +59,16 @@ private bool IsTextUnchanged(IQuickFixEditVisualization editViz, ITextSnapshot c } } - public interface IQuickFixEditVisualization + public interface ITextBasedQuickFixEditVisualization { IEdit Edit { get; } SnapshotSpan Span { get; } } - internal class QuickFixEditVisualization : IQuickFixEditVisualization + internal class TextBasedQuickFixEditVisualization : ITextBasedQuickFixEditVisualization { - public QuickFixEditVisualization(IEdit edit, SnapshotSpan span) + public TextBasedQuickFixEditVisualization(IEdit edit, SnapshotSpan span) { Edit = edit; Span = span; diff --git a/src/IssueViz/Models/TextBasedQuickFixApplication.cs b/src/IssueViz/Models/TextBasedQuickFixApplication.cs new file mode 100644 index 0000000000..a7156af2c8 --- /dev/null +++ b/src/IssueViz/Models/TextBasedQuickFixApplication.cs @@ -0,0 +1,51 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.VisualStudio.Text; +using SonarLint.VisualStudio.Infrastructure.VS; + +namespace SonarLint.VisualStudio.IssueVisualization.Models; + +internal class TextBasedQuickFixApplication(ITextBasedQuickFixVisualization textBasedQuickFixVisualization, ISpanTranslator spanTranslator) : IQuickFixApplication +{ + public ITextBasedQuickFixVisualization QuickFixVisualization { get; } = textBasedQuickFixVisualization; + + public string Message => QuickFixVisualization.Fix.Message; + + public bool CanBeApplied(ITextSnapshot currentSnapshot) => QuickFixVisualization.CanBeApplied(currentSnapshot); + + public Task ApplyAsync(ITextSnapshot currentSnapshot, CancellationToken cancellationToken) + { + var textBuffer = currentSnapshot.TextBuffer; + var textEdit = textBuffer.CreateEdit(); + + foreach (var edit in QuickFixVisualization.EditVisualizations) + { + var updatedSpan = spanTranslator.TranslateTo(edit.Span, textBuffer.CurrentSnapshot, SpanTrackingMode.EdgeExclusive); + + textEdit.Replace(updatedSpan, edit.Edit.NewText); + } + + cancellationToken.ThrowIfCancellationRequested(); + + textEdit.Apply(); + return Task.FromResult(true); + } +} diff --git a/src/IssueViz/Resources.Designer.cs b/src/IssueViz/Resources.Designer.cs index 4a1ac91eb0..eef4bf5ca2 100644 --- a/src/IssueViz/Resources.Designer.cs +++ b/src/IssueViz/Resources.Designer.cs @@ -1,7 +1,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -204,6 +203,33 @@ public static string ProductNameCommandPrefix { } } + /// + /// Looks up a localized string similar to Could not apply Quick Fix for {0}. Please reanalyze the file and try again.. + /// + public static string QuickFixSuggestedAction_CouldNotApply { + get { + return ResourceManager.GetString("QuickFixSuggestedAction_CouldNotApply", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "SonarQube QuickFix". + /// + public static string QuickFixSuggestedAction_CouldNotApplyMessageBoxCaption { + get { + return ResourceManager.GetString("QuickFixSuggestedAction_CouldNotApplyMessageBoxCaption", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Quick Fix. + /// + public static string QuickFixSuggestedAction_LogContext { + get { + return ResourceManager.GetString("QuickFixSuggestedAction_LogContext", resourceCulture); + } + } + /// /// Looks up a localized string similar to SonarQube: Show Issue Visualization ({0}). /// diff --git a/src/IssueViz/Resources.resx b/src/IssueViz/Resources.resx index 3fcc8a3bd5..290adbdd99 100644 --- a/src/IssueViz/Resources.resx +++ b/src/IssueViz/Resources.resx @@ -168,4 +168,13 @@ SonarQube: + + Could not apply Quick Fix for {0}. Please reanalyze the file and try again. + + + "SonarQube QuickFix" + + + Quick Fix + \ No newline at end of file diff --git a/src/IssueViz/packages.lock.json b/src/IssueViz/packages.lock.json index ebe8746688..c01fcaa900 100644 --- a/src/IssueViz/packages.lock.json +++ b/src/IssueViz/packages.lock.json @@ -118,16 +118,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -1135,8 +1125,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/IssueConverterTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/IssueConverterTests.cs deleted file mode 100644 index 36ac96b201..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/IssueConverterTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.Roslyn.Suppressions.InProcess; -using static SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.TestHelper; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.InProcess -{ - [TestClass] - public class IssueConverterTests - { - [TestMethod] - public void Convert_SimplePropertiesAreHandledCorrectly() - { - var sonarIssue = CreateSonarQubeIssue(hash: "aaaa", filePath: "\\bbb\\ccc\\file.txt", issueKey: "key"); - - var actual = IssueConverter.Convert(sonarIssue); - - actual.Hash.Should().Be("aaaa"); - actual.FilePath.Should().Be("\\bbb\\ccc\\file.txt"); - actual.IssueServerKey.Should().Be("key"); - } - - [TestMethod] - [DataRow(1, 0)] - [DataRow(2, 1)] - [DataRow(null, null)] - public void Convert_LineNumbersAreHandledCorrectly(int? sonarLineNumber, int? expected) - { - // 1-based Sonar line numbers should be converted to 0-based Roslyn line numbers - var sonarIssue = CreateSonarQubeIssue(line: sonarLineNumber); - - var actual = IssueConverter.Convert(sonarIssue); - - if (expected.HasValue) - { - actual.RoslynIssueLine.Value.Should().Be(expected); - } - else - { - actual.RoslynRuleId.Should().BeNull(); - } - } - - [TestMethod] - // Recognised repos - [DataRow("csharpsquid:S123", RoslynLanguage.CSharp, "S123")] - [DataRow("vbnet:S999", RoslynLanguage.VB, "S999")] - - // Valid but unrecognised repos - [DataRow("cpp:S111", RoslynLanguage.Unknown, "S111")] - [DataRow("javascript:S222", RoslynLanguage.Unknown, "S222")] - [DataRow("CSHARPSQUID:S333", RoslynLanguage.Unknown, "S333")] - [DataRow("VBNET:S444", RoslynLanguage.Unknown, "S444")] - - // Invalid keys - should not error - [DataRow("invalidcompositekey", RoslynLanguage.Unknown, null)] - [DataRow("invalid::compositekey", RoslynLanguage.Unknown, ":compositekey")] - [DataRow(":invalid", RoslynLanguage.Unknown, "invalid")] - [DataRow("csharpsquid:", RoslynLanguage.CSharp, "")] - public void Convert_RepoKeysAreHandledCorrectly(string compositeKey, RoslynLanguage expectedLanguage, string expectedRuleKey) - { - var sonarIssue = CreateSonarQubeIssue(ruleId: compositeKey); - - var actual = IssueConverter.Convert(sonarIssue); - - actual.RoslynLanguage.Should().Be(expectedLanguage); - actual.RoslynRuleId.Should().Be(expectedRuleKey); - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/NewSuppressedIssuesCalculatorTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/NewSuppressedIssuesCalculatorTests.cs deleted file mode 100644 index ca7871e1fc..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/NewSuppressedIssuesCalculatorTests.cs +++ /dev/null @@ -1,104 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Roslyn.Suppressions.InProcess; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; -using SonarQube.Client.Models; -using static SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.TestHelper; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.InProcess; - -[TestClass] -public class NewSuppressedIssuesCalculatorTests : SuppressedIssuesCalculatorTestsBase -{ - [TestInitialize] - public void TestInitialize() - { - RoslynSettingsFileStorage = Substitute.For(); - Logger = Substitute.For(); - Logger.ForContext(Arg.Any()).Returns(Logger); - } - - [TestMethod] - public void CreateNewSuppressedIssuesCalculator_IssueDoNotExist_IssuesAreConvertedAndFiltered() - { - var testSubject = CreateNewSuppressedIssuesCalculator([ - CsharpIssueSuppressed, // C# issue - VbNetIssueSuppressed, // VB issue - CppIssueSuppressed, // C++ issue - ignored - UnknownRepoIssue, // unrecognised repo - ignored - InvalidRepoKeyIssue, // invalid repo key - ignored - NoRuleIdIssue // invalid repo key (no rule id) - ignored - ]); - - var result = testSubject.GetSuppressedIssuesOrNull(RoslynSettingsKey); - - VerifyExpectedSuppressions([.. result], [CsharpIssueSuppressed, VbNetIssueSuppressed]); - Logger.Received(1).LogVerbose(Resources.Strings.RoslynSettingsFileSynchronizerAddNewSuppressions); - } - - [TestMethod] - public void CreateNewSuppressedIssuesCalculator_IssueDoNotExist_OnlySuppressedIssuesAreInSettings() - { - MockExistingSuppressionsOnSettingsFile(); - var testSubject = CreateNewSuppressedIssuesCalculator([ - CsharpIssueSuppressed, - VbNetIssueSuppressed, - CsharpIssueNotSuppressed, - VbNetIssueNotSuppressed, - ]); - - var result = testSubject.GetSuppressedIssuesOrNull(RoslynSettingsKey); - - VerifyExpectedSuppressions([.. result], [CsharpIssueSuppressed, VbNetIssueSuppressed]); - Logger.Received(1).LogVerbose(Resources.Strings.RoslynSettingsFileSynchronizerAddNewSuppressions); - } - - [TestMethod] - public void CreateNewSuppressedIssuesCalculator_IssuesExist_UpdatesCorrectly() - { - var newSonarQubeIssues = new[] { CsharpIssueSuppressed, VbNetIssueSuppressed }; - MockExistingSuppressionsOnSettingsFile(newSonarQubeIssues); - var testSubject = CreateNewSuppressedIssuesCalculator(newSonarQubeIssues); - - var result = testSubject.GetSuppressedIssuesOrNull(RoslynSettingsKey); - - VerifyExpectedSuppressions([.. result], newSonarQubeIssues); - Logger.Received(1).LogVerbose(Resources.Strings.RoslynSettingsFileSynchronizerAddNewSuppressions); - } - - [TestMethod] - public void CreateNewSuppressedIssuesCalculator_TwoNewIssuesAdded_DoesNotRemoveExistingOnes() - { - var newSonarQubeIssues = new[] { CreateSonarQubeIssue("csharpsquid:S666"), CreateSonarQubeIssue("vbnet:S666") }; - var existingIssues = new[] { CsharpIssueSuppressed, VbNetIssueSuppressed, }; - MockExistingSuppressionsOnSettingsFile(existingIssues); - var testSubject = CreateNewSuppressedIssuesCalculator(newSonarQubeIssues); - - var result = testSubject.GetSuppressedIssuesOrNull(RoslynSettingsKey); - - var expectedIssues = existingIssues.Union(newSonarQubeIssues).ToArray(); - VerifyExpectedSuppressions([.. result], expectedIssues); - Logger.Received(1).LogVerbose(Resources.Strings.RoslynSettingsFileSynchronizerAddNewSuppressions); - } - - private NewSuppressedIssuesCalculator CreateNewSuppressedIssuesCalculator(IEnumerable sonarQubeIssues) => new(Logger, RoslynSettingsFileStorage, sonarQubeIssues); -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/RoslynSettingsFileSynchronizerTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/RoslynSettingsFileSynchronizerTests.cs deleted file mode 100644 index bda105d340..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/RoslynSettingsFileSynchronizerTests.cs +++ /dev/null @@ -1,405 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.IO; -using SonarLint.VisualStudio.ConnectedMode.Persistence; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.Roslyn.Suppressions.InProcess; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client.Models; -using static SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.TestHelper; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.InProcess; - -[TestClass] -public class RoslynSettingsFileSynchronizerTests -{ - private const string DefaultSln = "DefaultSolution"; - private IConfigurationProvider configProvider; - private ISuppressedIssuesCalculatorFactory suppressedIssuesCalculatorFactory; - private IRoslynSettingsFileStorage roslynSettingsFileStorage; - private ISolutionInfoProvider solutionInfoProvider; - private RoslynSettingsFileSynchronizer testSubject; - private IThreadHandling threadHandling; - private readonly BindingConfiguration connectedBindingConfiguration = CreateConnectedConfiguration("some project key"); - private ISolutionBindingRepository solutionBindingRepository; - private IRoslynSuppressionUpdater roslynSuppressionUpdater; - private ISuppressedIssuesCalculator suppressedIssuesCalculator; - - private readonly SonarQubeIssue csharpIssueSuppressed = CreateSonarQubeIssue("csharpsquid:S111"); - private readonly SonarQubeIssue vbNetIssueSuppressed = CreateSonarQubeIssue("vbnet:S222"); - - [TestInitialize] - public void TestInitialize() - { - roslynSettingsFileStorage = Substitute.For(); - configProvider = Substitute.For(); - solutionInfoProvider = Substitute.For(); - solutionBindingRepository = Substitute.For(); - roslynSuppressionUpdater = Substitute.For(); - threadHandling = new NoOpThreadHandler(); - MockSuppressedIssuesCalculator(); - - testSubject = CreateTestSubject(threadHandling); - MockSolutionInfoProvider(DefaultSln); - } - - [TestMethod] - public void MefCtor_CheckIsExported() => - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - - [TestMethod] - public void MefCtor_CheckTypeIsNonShared() => MefTestHelpers.CheckIsNonSharedMefComponent(); - - [TestMethod] - public void Ctor_RegisterToEvents() - { - solutionBindingRepository.Received(1).BindingDeleted += Arg.Any>(); - - roslynSuppressionUpdater.Received(1).SuppressedIssuesReloaded += Arg.Any>(); - roslynSuppressionUpdater.Received(1).NewIssuesSuppressed += Arg.Any>(); - roslynSuppressionUpdater.Received(1).SuppressionsRemoved += Arg.Any>(); - roslynSuppressionUpdater.ReceivedCalls().Should().HaveCount(3); - } - - [TestMethod] - public void Dispose_UnregisterFromEvents() - { - testSubject.Dispose(); - - solutionBindingRepository.Received(1).BindingDeleted -= Arg.Any>(); - roslynSuppressionUpdater.Received(1).SuppressedIssuesReloaded -= Arg.Any>(); - roslynSuppressionUpdater.Received(1).NewIssuesSuppressed -= Arg.Any>(); - roslynSuppressionUpdater.Received(1).SuppressionsRemoved -= Arg.Any>(); - } - - [TestMethod] - public void BindingDeleted_StorageFileDeleted() - { - var localBindingKey = "my solution name"; - solutionBindingRepository.BindingDeleted += Raise.EventWith(new LocalBindingKeyEventArgs(localBindingKey)); - - roslynSettingsFileStorage.Received(1).Delete(localBindingKey); - } - - [TestMethod] - public void SuppressedIssuesReloaded_StandaloneMode_StorageFileDeleted() - { - MockConfigProvider(BindingConfiguration.Standalone); - var sonarQubeIssues = new[] { csharpIssueSuppressed, vbNetIssueSuppressed }; - - RaiseSuppressedIssuesReloaded(sonarQubeIssues); - - configProvider.Received(1).GetConfiguration(); - roslynSettingsFileStorage.Received(1).Delete(DefaultSln); - roslynSettingsFileStorage.ReceivedCalls().Should().HaveCount(1); - suppressedIssuesCalculatorFactory.Received(1).CreateAllSuppressedIssuesCalculator(sonarQubeIssues); - suppressedIssuesCalculator.DidNotReceive().GetSuppressedIssuesOrNull(Arg.Any()); - } - - [TestMethod] - [DataRow(true)] - [DataRow(false)] // should update storage even when there are no issues - public void SuppressedIssuesReloaded_ConnectedMode_StorageUpdated(bool hasIssues) - { - MockConfigProvider(connectedBindingConfiguration); - var issues = hasIssues ? new[] { csharpIssueSuppressed, vbNetIssueSuppressed } : Array.Empty(); - - RaiseSuppressedIssuesReloaded(issues); - - roslynSettingsFileStorage.Received(1).Update(Arg.Any(), DefaultSln); - suppressedIssuesCalculatorFactory.Received(1).CreateAllSuppressedIssuesCalculator(issues); - suppressedIssuesCalculator.Received(1).GetSuppressedIssuesOrNull(DefaultSln); - } - - [TestMethod] - public void SuppressedIssuesReloaded_FileStorageIsUpdatedOnBackgroundThread() - { - MockConfigProvider(connectedBindingConfiguration); - var threadHandlingMock = Substitute.For(); - CreateTestSubject(threadHandlingMock); - - RaiseSuppressedIssuesReloaded([csharpIssueSuppressed]); - - threadHandlingMock.Received(1).RunOnBackgroundThread(Arg.Any>>()); - } - - [TestMethod] - public void SuppressedIssuesReloaded_NoSolution_DoesNothing() - { - MockSolutionInfoProvider(null); - var sonarQubeIssues = new[] { csharpIssueSuppressed, vbNetIssueSuppressed }; - - RaiseSuppressedIssuesReloaded(sonarQubeIssues); - - solutionInfoProvider.Received(1).GetSolutionNameAsync(); - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Update(default, default); - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Delete(default); - configProvider.DidNotReceiveWithAnyArgs().GetConfiguration(); - suppressedIssuesCalculatorFactory.Received(1).CreateAllSuppressedIssuesCalculator(sonarQubeIssues); - suppressedIssuesCalculator.DidNotReceive().GetSuppressedIssuesOrNull(Arg.Any()); - } - - [TestMethod] - public void SuppressedIssuesReloaded_ConnectedMode_UpdatesFileStorage() - { - MockConfigProvider(connectedBindingConfiguration); - var sonarQubeIssues = new[] { csharpIssueSuppressed, vbNetIssueSuppressed }; - var expectedSonarQubeIssues = new[] { CreateIssue(issueServerKey: csharpIssueSuppressed.IssueKey) }; - suppressedIssuesCalculator.GetSuppressedIssuesOrNull(DefaultSln).Returns(expectedSonarQubeIssues); - - RaiseSuppressedIssuesReloaded(sonarQubeIssues); - - roslynSettingsFileStorage.Received(1) - .Update(Arg.Is(x => VerifyExpectedRoslynSettings(x, expectedSonarQubeIssues)), DefaultSln); - suppressedIssuesCalculatorFactory.Received(1).CreateAllSuppressedIssuesCalculator(sonarQubeIssues); - suppressedIssuesCalculator.Received(1).GetSuppressedIssuesOrNull(DefaultSln); - } - - [TestMethod] - public void SuppressedIssuesReloaded_SuppressedIssueCalculatorReturnsNull_DoesNothing() - { - MockConfigProvider(connectedBindingConfiguration); - suppressedIssuesCalculator.GetSuppressedIssuesOrNull(DefaultSln).Returns((IEnumerable)null); - - RaiseSuppressedIssuesReloaded([csharpIssueSuppressed, vbNetIssueSuppressed]); - - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Update(default, default); - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Delete(default); - } - - [TestMethod] - public void NewIssuesSuppressed_StandaloneMode_StorageFileDeleted() - { - MockConfigProvider(BindingConfiguration.Standalone); - var newSonarQubeIssues = new[] { csharpIssueSuppressed }; - - RaiseNewIssuesSuppressed(newSonarQubeIssues); - - configProvider.Received(1).GetConfiguration(); - roslynSettingsFileStorage.Received(1).Delete(DefaultSln); - roslynSettingsFileStorage.ReceivedCalls().Should().HaveCount(1); - suppressedIssuesCalculatorFactory.Received(1).CreateNewSuppressedIssuesCalculator(newSonarQubeIssues); - suppressedIssuesCalculator.DidNotReceive().GetSuppressedIssuesOrNull(Arg.Any()); - } - - [TestMethod] - public void NewIssuesSuppressed_NoIssues_StorageNotUpdated() - { - MockConfigProvider(connectedBindingConfiguration); - - RaiseNewIssuesSuppressed([]); - - roslynSettingsFileStorage.DidNotReceive().Update(Arg.Any(), DefaultSln); - suppressedIssuesCalculatorFactory.DidNotReceive().CreateNewSuppressedIssuesCalculator(Arg.Any()); - suppressedIssuesCalculator.DidNotReceive().GetSuppressedIssuesOrNull(Arg.Any()); - } - - [TestMethod] - public void NewIssuesSuppressed_FileStorageIsUpdatedOnBackgroundThread() - { - MockConfigProvider(connectedBindingConfiguration); - var threadHandlingMock = Substitute.For(); - CreateTestSubject(threadHandlingMock); - - RaiseNewIssuesSuppressed([csharpIssueSuppressed]); - - threadHandlingMock.Received(1).RunOnBackgroundThread(Arg.Any>>()); - } - - [TestMethod] - public void NewIssuesSuppressed_NoSolution_DoesNothing() - { - MockSolutionInfoProvider(null); - var newSonarQubeIssues = new[] { csharpIssueSuppressed }; - - RaiseNewIssuesSuppressed(newSonarQubeIssues); - - solutionInfoProvider.Received(1).GetSolutionNameAsync(); - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Update(default, default); - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Delete(default); - configProvider.DidNotReceiveWithAnyArgs().GetConfiguration(); - suppressedIssuesCalculatorFactory.Received(1).CreateNewSuppressedIssuesCalculator(newSonarQubeIssues); - suppressedIssuesCalculator.DidNotReceive().GetSuppressedIssuesOrNull(Arg.Any()); - } - - [TestMethod] - public void NewIssuesSuppressed_ConnectedMode_UpdatesFileStorage() - { - MockConfigProvider(connectedBindingConfiguration); - var sonarQubeIssues = new[] { csharpIssueSuppressed, vbNetIssueSuppressed }; - var expectedSonarQubeIssues = new[] { CreateIssue(issueServerKey: csharpIssueSuppressed.IssueKey) }; - suppressedIssuesCalculator.GetSuppressedIssuesOrNull(DefaultSln).Returns(expectedSonarQubeIssues); - - RaiseNewIssuesSuppressed(sonarQubeIssues); - - roslynSettingsFileStorage.Received(1) - .Update(Arg.Is(x => VerifyExpectedRoslynSettings(x, expectedSonarQubeIssues)), DefaultSln); - suppressedIssuesCalculatorFactory.Received(1).CreateNewSuppressedIssuesCalculator(sonarQubeIssues); - suppressedIssuesCalculator.Received(1).GetSuppressedIssuesOrNull(DefaultSln); - } - - [TestMethod] - public void NewIssuesSuppressed_SuppressedIssueCalculatorReturnsNull_DoesNothing() - { - MockConfigProvider(connectedBindingConfiguration); - suppressedIssuesCalculator.GetSuppressedIssuesOrNull(DefaultSln).Returns((IEnumerable)null); - - RaiseNewIssuesSuppressed([csharpIssueSuppressed, vbNetIssueSuppressed]); - - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Update(default, default); - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Delete(default); - } - - [TestMethod] - public void NewIssuesResolved_StandaloneMode_StorageFileDeleted() - { - MockConfigProvider(BindingConfiguration.Standalone); - var newSonarQubeIssues = new[] { csharpIssueSuppressed.IssueKey }; - - RaiseSuppressionsRemoved(newSonarQubeIssues); - - configProvider.Received(1).GetConfiguration(); - roslynSettingsFileStorage.Received(1).Delete(Path.GetFileNameWithoutExtension(DefaultSln)); - roslynSettingsFileStorage.ReceivedCalls().Should().HaveCount(1); - suppressedIssuesCalculatorFactory.Received(1).CreateSuppressedIssuesRemovedCalculator(newSonarQubeIssues); - suppressedIssuesCalculator.DidNotReceive().GetSuppressedIssuesOrNull(Arg.Any()); - } - - [TestMethod] - public void NewIssuesResolved_NoIssues_StorageNotUpdated() - { - MockConfigProvider(connectedBindingConfiguration); - - RaiseSuppressionsRemoved([]); - - solutionInfoProvider.DidNotReceive().GetSolutionNameAsync(); - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Update(default, default); - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Delete(default); - configProvider.DidNotReceiveWithAnyArgs().GetConfiguration(); - suppressedIssuesCalculatorFactory.DidNotReceive().CreateSuppressedIssuesRemovedCalculator(Arg.Any()); - suppressedIssuesCalculator.DidNotReceive().GetSuppressedIssuesOrNull(Arg.Any()); - } - - [TestMethod] - public void NewIssuesResolved_FileStorageIsUpdatedOnBackgroundThread() - { - MockConfigProvider(connectedBindingConfiguration); - var threadHandlingMock = Substitute.For(); - CreateTestSubject(threadHandlingMock); - - RaiseSuppressionsRemoved([csharpIssueSuppressed.IssueKey]); - - threadHandlingMock.Received(1).RunOnBackgroundThread(Arg.Any>>()); - } - - [TestMethod] - public void NewIssuesResolved_NoSolution_DoesNothing() - { - MockSolutionInfoProvider(null); - var newSonarQubeIssues = new[] { csharpIssueSuppressed.IssueKey }; - - RaiseSuppressionsRemoved(newSonarQubeIssues); - - solutionInfoProvider.Received(1).GetSolutionNameAsync(); - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Update(default, default); - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Delete(default); - configProvider.DidNotReceiveWithAnyArgs().GetConfiguration(); - suppressedIssuesCalculatorFactory.Received().CreateSuppressedIssuesRemovedCalculator(newSonarQubeIssues); - suppressedIssuesCalculator.DidNotReceive().GetSuppressedIssuesOrNull(Arg.Any()); - } - - [TestMethod] - public void NewIssuesResolved_ConnectedMode_UpdatesFileStorage() - { - MockConfigProvider(connectedBindingConfiguration); - var sonarQubeIssues = new[] { csharpIssueSuppressed.IssueKey, vbNetIssueSuppressed.IssueKey }; - var expectedSonarQubeIssues = new[] { CreateIssue(issueServerKey: csharpIssueSuppressed.IssueKey) }; - suppressedIssuesCalculator.GetSuppressedIssuesOrNull(DefaultSln).Returns(expectedSonarQubeIssues); - - RaiseSuppressionsRemoved(sonarQubeIssues); - - roslynSettingsFileStorage.Received(1) - .Update(Arg.Is(x => VerifyExpectedRoslynSettings(x, expectedSonarQubeIssues)), DefaultSln); - suppressedIssuesCalculatorFactory.Received(1).CreateSuppressedIssuesRemovedCalculator(sonarQubeIssues); - suppressedIssuesCalculator.Received(1).GetSuppressedIssuesOrNull(DefaultSln); - } - - [TestMethod] - public void NewIssuesResolved_SuppressedIssueCalculatorReturnsNull_DoesNothing() - { - MockConfigProvider(connectedBindingConfiguration); - suppressedIssuesCalculator.GetSuppressedIssuesOrNull(DefaultSln).Returns((IEnumerable)null); - - RaiseSuppressionsRemoved([csharpIssueSuppressed.IssueKey, vbNetIssueSuppressed.IssueKey]); - - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Update(default, default); - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Delete(default); - } - - private void MockConfigProvider(BindingConfiguration configuration) => configProvider.GetConfiguration().Returns(configuration); - - private static BindingConfiguration CreateConnectedConfiguration(string projectKey) - { - var project = new BoundServerProject("solution", projectKey, new ServerConnection.SonarQube(new Uri("http://bound"))); - - return BindingConfiguration.CreateBoundConfiguration(project, SonarLintMode.Connected, "some directory"); - } - - private void MockSolutionInfoProvider(string solutionName) => solutionInfoProvider.GetSolutionNameAsync().Returns(solutionName); - - private void RaiseSuppressedIssuesReloaded(SonarQubeIssue[] issues) => roslynSuppressionUpdater.SuppressedIssuesReloaded += Raise.EventWith(null, new SuppressionsEventArgs(issues)); - - private void RaiseNewIssuesSuppressed(SonarQubeIssue[] issues) => roslynSuppressionUpdater.NewIssuesSuppressed += Raise.EventWith(null, new SuppressionsEventArgs(issues)); - - private void RaiseSuppressionsRemoved(string[] issueKeys) => roslynSuppressionUpdater.SuppressionsRemoved += Raise.EventWith(null, new SuppressionsRemovedEventArgs(issueKeys)); - - private bool VerifyExpectedRoslynSettings(RoslynSettings roslynSettings, IEnumerable expectedSuppressedIssues) => - roslynSettings.SonarProjectKey == connectedBindingConfiguration.Project.ServerProjectKey - && roslynSettings.Suppressions.SequenceEqual(expectedSuppressedIssues); - - private void MockSuppressedIssuesCalculator() - { - suppressedIssuesCalculator = Substitute.For(); - suppressedIssuesCalculatorFactory = Substitute.For(); - suppressedIssuesCalculatorFactory.CreateAllSuppressedIssuesCalculator(Arg.Any()).Returns(suppressedIssuesCalculator); - suppressedIssuesCalculatorFactory.CreateNewSuppressedIssuesCalculator(Arg.Any()).Returns(suppressedIssuesCalculator); - suppressedIssuesCalculatorFactory.CreateSuppressedIssuesRemovedCalculator(Arg.Any()).Returns(suppressedIssuesCalculator); - } - - private RoslynSettingsFileSynchronizer CreateTestSubject(IThreadHandling mockedThreadHandling) => - new( - roslynSettingsFileStorage, - configProvider, - solutionInfoProvider, - solutionBindingRepository, - roslynSuppressionUpdater, - suppressedIssuesCalculatorFactory, - mockedThreadHandling); -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesCalculatorFactoryTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesCalculatorFactoryTests.cs deleted file mode 100644 index 76184ae9d5..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesCalculatorFactoryTests.cs +++ /dev/null @@ -1,87 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Roslyn.Suppressions.InProcess; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; -using SonarLint.VisualStudio.TestInfrastructure; -using static SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.TestHelper; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.InProcess; - -[TestClass] -public class SuppressedIssuesCalculatorFactoryTests -{ - private IRoslynSettingsFileStorage roslynSettingsFileStorage; - private ILogger logger; - private SuppressedIssuesCalculatorFactory testSubject; - - [TestInitialize] - public void TestInitialize() - { - roslynSettingsFileStorage = Substitute.For(); - logger = Substitute.For(); - logger.ForContext(Arg.Any()).Returns(logger); - - testSubject = new SuppressedIssuesCalculatorFactory(logger, roslynSettingsFileStorage); - } - - [TestMethod] - public void MefCtor_CheckIsExported() => - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - - [TestMethod] - public void MefCtor_CheckTypeIsNonShared() => MefTestHelpers.CheckIsNonSharedMefComponent(); - - [TestMethod] - public void Ctor_SetsLogContext() => logger.Received(1).ForContext(SuppressedIssuesCalculatorFactory.SuppressedIssuesCalculatorLogContext); - - [TestMethod] - public void CreateAllSuppressedIssuesCalculator_CreatesAllSuppressedIssuesCalculator() - { - var sonarQubeIssues = new[] { CreateSonarQubeIssue("csharpsquid:S111") }; - - var calculator = testSubject.CreateAllSuppressedIssuesCalculator(sonarQubeIssues); - - calculator.Should().BeOfType(); - } - - [TestMethod] - public void CreateNewSuppressedIssuesCalculator_CreatesNewSuppressedIssuesCalculator() - { - var sonarQubeIssues = new[] { CreateSonarQubeIssue("csharpsquid:S111") }; - - var calculator = testSubject.CreateNewSuppressedIssuesCalculator(sonarQubeIssues); - - calculator.Should().BeOfType(); - } - - [TestMethod] - public void CreateSuppressedIssuesRemovedCalculator_CreatesSuppressedIssuesRemovedCalculator() - { - var issueServerKeys = new[] { Guid.NewGuid().ToString() }; - - var calculator = testSubject.CreateSuppressedIssuesRemovedCalculator(issueServerKeys); - - calculator.Should().BeOfType(); - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesCalculatorTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesCalculatorTests.cs deleted file mode 100644 index 696572f90d..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesCalculatorTests.cs +++ /dev/null @@ -1,74 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Roslyn.Suppressions.InProcess; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.InProcess; - -[TestClass] -public class SuppressedIssuesCalculatorTests : SuppressedIssuesCalculatorTestsBase -{ - [TestInitialize] - public void TestInitialize() - { - RoslynSettingsFileStorage = Substitute.For(); - Logger = Substitute.For(); - Logger.ForContext(Arg.Any()).Returns(Logger); - } - - [TestMethod] - public void AllSuppressedIssuesCalculator_IssuesAreConvertedAndFiltered() - { - var testSubject = CreateAllSuppressedIssuesCalculator([ - CsharpIssueSuppressed, // C# issue - VbNetIssueSuppressed, // VB issue - CppIssueSuppressed, // C++ issue - ignored - UnknownRepoIssue, // unrecognised repo - ignored - InvalidRepoKeyIssue, // invalid repo key - ignored - NoRuleIdIssue // invalid repo key (no rule id) - ignored - ]); - - var result = testSubject.GetSuppressedIssuesOrNull(RoslynSettingsKey); - - VerifyExpectedSuppressions([.. result], [CsharpIssueSuppressed, VbNetIssueSuppressed]); - Logger.Received(1).LogVerbose(Resources.Strings.RoslynSettingsFileSynchronizerReloadSuppressions); - } - - [TestMethod] - public void AllSuppressedIssuesCalculator_OnlySuppressedIssuesAreInSettings() - { - var testSubject = CreateAllSuppressedIssuesCalculator([ - CsharpIssueSuppressed, - VbNetIssueSuppressed, - CsharpIssueNotSuppressed, - VbNetIssueNotSuppressed, - ]); - - var result = testSubject.GetSuppressedIssuesOrNull(RoslynSettingsKey); - - VerifyExpectedSuppressions([.. result], [CsharpIssueSuppressed, VbNetIssueSuppressed]); - Logger.Received(1).LogVerbose(Resources.Strings.RoslynSettingsFileSynchronizerReloadSuppressions); - } - - private AllSuppressedIssuesCalculator CreateAllSuppressedIssuesCalculator(IEnumerable sonarQubeIssues) => new(Logger, sonarQubeIssues); -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesCalculatorTestsBase.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesCalculatorTestsBase.cs deleted file mode 100644 index 2229430f59..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesCalculatorTestsBase.cs +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Roslyn.Suppressions.InProcess; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; -using SonarQube.Client.Models; -using static SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.TestHelper; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.InProcess; - -public class SuppressedIssuesCalculatorTestsBase -{ - internal const string RoslynSettingsKey = "my solution"; - internal IRoslynSettingsFileStorage RoslynSettingsFileStorage; - internal ILogger Logger; - - protected readonly SonarQubeIssue CsharpIssueSuppressed = CreateSonarQubeIssue("csharpsquid:S111"); - protected readonly SonarQubeIssue VbNetIssueSuppressed = CreateSonarQubeIssue("vbnet:S222"); - protected readonly SonarQubeIssue CppIssueSuppressed = CreateSonarQubeIssue("cpp:S333"); - protected readonly SonarQubeIssue UnknownRepoIssue = CreateSonarQubeIssue("xxx:S444"); - protected readonly SonarQubeIssue InvalidRepoKeyIssue = CreateSonarQubeIssue("xxxS555"); - protected readonly SonarQubeIssue NoRuleIdIssue = CreateSonarQubeIssue("xxx:"); - protected readonly SonarQubeIssue CsharpIssueNotSuppressed = CreateSonarQubeIssue("csharpsquid:S333", isSuppressed: false); - protected readonly SonarQubeIssue VbNetIssueNotSuppressed = CreateSonarQubeIssue("vbnet:S444", isSuppressed: false); - - internal static void VerifyExpectedSuppressions(SuppressedIssue[] actualSuppressions, SonarQubeIssue[] expectedSuppressions) - { - actualSuppressions.Should().HaveCount(expectedSuppressions.Length); - actualSuppressions.Should().BeEquivalentTo(expectedSuppressions.Select(IssueConverter.Convert)); - } - - protected void MockExistingSuppressionsOnSettingsFile(params SonarQubeIssue[] existingIssues) => - RoslynSettingsFileStorage.Get(Arg.Any()).Returns(existingIssues.Length == 0 - ? RoslynSettings.Empty - : new RoslynSettings { SonarProjectKey = RoslynSettingsKey, Suppressions = existingIssues.Select(IssueConverter.Convert) }); -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesRemovedCalculatorTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesRemovedCalculatorTests.cs deleted file mode 100644 index 7eec1dad57..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesRemovedCalculatorTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Roslyn.Suppressions.InProcess; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; -using static SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.TestHelper; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.InProcess; - -[TestClass] -public class SuppressedIssuesRemovedCalculatorTests : SuppressedIssuesCalculatorTestsBase -{ - [TestInitialize] - public void TestInitialize() - { - RoslynSettingsFileStorage = Substitute.For(); - Logger = Substitute.For(); - Logger.ForContext(Arg.Any()).Returns(Logger); - } - - [TestMethod] - public void SuppressedIssuesRemovedCalculator_IssueDoNotExistInFile_DoesNotUpdateFile() - { - MockExistingSuppressionsOnSettingsFile(); - var newSonarQubeIssues = new[] - { - CsharpIssueSuppressed.IssueKey, // C# issue - VbNetIssueSuppressed.IssueKey, // VB issue - CppIssueSuppressed.IssueKey, // C++ issue - ignored - UnknownRepoIssue.IssueKey, // unrecognised repo - ignored - InvalidRepoKeyIssue.IssueKey, // invalid repo key - ignored - NoRuleIdIssue.IssueKey // invalid repo key (no rule id) - ignored - }; - var testSubject = CreateSuppressedIssuesRemovedCalculator(newSonarQubeIssues); - - var result = testSubject.GetSuppressedIssuesOrNull(RoslynSettingsKey); - - result.Should().BeNull(); - RoslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Update(default, default); - Logger.DidNotReceive().LogVerbose(Resources.Strings.RoslynSettingsFileSynchronizerRemoveSuppressions); - } - - [TestMethod] - public void SuppressedIssuesRemovedCalculator_IssueKeysExistInFile_RemovesIssues() - { - var newSonarQubeIssues = new[] { CsharpIssueSuppressed, VbNetIssueSuppressed }; - MockExistingSuppressionsOnSettingsFile(newSonarQubeIssues); - var testSubject = CreateSuppressedIssuesRemovedCalculator(newSonarQubeIssues.Select(x => x.IssueKey).ToArray()); - - var result = testSubject.GetSuppressedIssuesOrNull(RoslynSettingsKey); - - VerifyExpectedSuppressions([.. result], []); - Logger.Received(1).LogVerbose(Resources.Strings.RoslynSettingsFileSynchronizerRemoveSuppressions); - } - - [TestMethod] - public void SuppressedIssuesRemovedCalculator_OneNewIssueResolved_DoesNotRemoveExistingOne() - { - var existingIssues = new[] { CsharpIssueSuppressed, VbNetIssueSuppressed, }; - MockExistingSuppressionsOnSettingsFile(existingIssues); - var testSubject = CreateSuppressedIssuesRemovedCalculator([CsharpIssueSuppressed.IssueKey]); - - var result = testSubject.GetSuppressedIssuesOrNull(RoslynSettingsKey); - - VerifyExpectedSuppressions([.. result], [VbNetIssueSuppressed]); - Logger.Received(1).LogVerbose(Resources.Strings.RoslynSettingsFileSynchronizerRemoveSuppressions); - } - - [TestMethod] - public void SuppressedIssuesRemovedCalculator_MultipleIssuesWithSameIssueServerKeyExistsInFile_RemovesThemAll() - { - var existingIssues = new[] { CsharpIssueSuppressed, CreateSonarQubeIssue(issueKey: CsharpIssueSuppressed.IssueKey), CreateSonarQubeIssue(issueKey: CsharpIssueSuppressed.IssueKey) }; - MockExistingSuppressionsOnSettingsFile(existingIssues); - - var testSubject = CreateSuppressedIssuesRemovedCalculator([CsharpIssueSuppressed.IssueKey]); - - var result = testSubject.GetSuppressedIssuesOrNull(RoslynSettingsKey); - - VerifyExpectedSuppressions([.. result], []); - Logger.Received(1).LogVerbose(Resources.Strings.RoslynSettingsFileSynchronizerRemoveSuppressions); - } - - private SuppressedIssuesRemovedCalculator CreateSuppressedIssuesRemovedCalculator(IEnumerable serverIssueKeys) => new(Logger, RoslynSettingsFileStorage, serverIssueKeys); -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/Roslyn.Suppressions.UnitTests.csproj b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/Roslyn.Suppressions.UnitTests.csproj deleted file mode 100644 index a38b04e5d5..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/Roslyn.Suppressions.UnitTests.csproj +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - {C478DAE7-58BC-4D02-929E-E413B40F2517} - SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests - SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests - - - - - - - - - - - TestParallelization.cs - - - - diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/RoslynSettingsFileInfoTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/RoslynSettingsFileInfoTests.cs deleted file mode 100644 index 3a9ba5279e..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/RoslynSettingsFileInfoTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Globalization; -using System.IO; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SonarLint.VisualStudio.TestInfrastructure.Helpers; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests -{ - [TestClass] - public class RoslynSettingsFileInfoTests - { - - [DataRow("projectkey", "projectkey")]//Testing lower case - [DataRow("projectKEY", "projectkey")]//Testing upper case - [DataRow("III", "iii")]//Testing upper case with invariant culture - [DataRow("project:key", "project_key")]//Testing illegal characters - [TestMethod] - public void GetSettingsFilePath_ReturnsFilePathCorrectly(string projectKey, string expectedFileName) - { - var expectedPath = Path.Combine(RoslynSettingsFileInfo.Directory, expectedFileName + ".json"); - - //This is to make sure normalising the keys done correctly with invariant culture - //https://en.wikipedia.org/wiki/Dotted_and_dotless_I - using var scope = new TurkishCultureScope(); - - var actualPath = RoslynSettingsFileInfo.GetSettingsFilePath(projectKey); - - actualPath.Should().Be(expectedPath); - - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/RoslynSettingsFileStorageTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/RoslynSettingsFileStorageTests.cs deleted file mode 100644 index 9fc744ee91..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/RoslynSettingsFileStorageTests.cs +++ /dev/null @@ -1,271 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.IO.Abstractions; -using Moq; -using Newtonsoft.Json; -using NSubstitute.ExceptionExtensions; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Roslyn.Suppressions.Resources; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; -using SonarLint.VisualStudio.TestInfrastructure; -using static SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.TestHelper; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests; - -[TestClass] -public class RoslynSettingsFileStorageTests -{ - private const string SolutionName = "a solution name"; - private IFile file; - private IFileSystem fileSystem; - private TestLogger logger; - private RoslynSettingsFileStorage testSubject; - - [TestInitialize] - public void TestInitialize() - { - logger = new TestLogger(); - file = Substitute.For(); - fileSystem = Substitute.For(); - testSubject = new RoslynSettingsFileStorage(logger, fileSystem); - - MockFileSystem(); - } - - [TestMethod] - public void MefCtor_CheckIsExported() => - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport()); - - [TestMethod] - public void Update_HasIssues_IssuesWrittenToFile() - { - var settings = new RoslynSettings { SonarProjectKey = "projectKey", Suppressions = new[] { CreateIssue("issue1") } }; - - testSubject.Update(settings, SolutionName); - - CheckFileWritten(settings, SolutionName); - logger.AssertNoOutputMessages(); - } - - [TestMethod] - public void Get_HasIssues_IssuesReadFromFile() - { - var issue1 = CreateIssue("key1"); - var issue2 = CreateIssue("key2"); - var settings = new RoslynSettings { SonarProjectKey = "projectKey", Suppressions = new[] { issue1, issue2 } }; - MockFileReadAllText(settings); - - var actual = testSubject.Get("projectKey"); - - var issuesGotten = actual.Suppressions.ToList(); - - file.Received(1).ReadAllText(GetFilePath("projectKey")); - logger.AssertNoOutputMessages(); - - issuesGotten.Count.Should().Be(2); - issuesGotten[0].RoslynRuleId.Should().Be(issue1.RoslynRuleId); - issuesGotten[1].RoslynRuleId.Should().Be(issue2.RoslynRuleId); - } - - [TestMethod] - public void Update_SolutionNameHasInvalidChars_InvalidCharsReplaced() - { - var settings = new RoslynSettings { SonarProjectKey = "project:key" }; - - testSubject.Update(settings, "my:solution"); - - CheckFileWritten(settings, "my_solution"); - logger.AssertNoOutputMessages(); - } - - [TestMethod] - public void Update_ErrorOccuredWhenWritingFile_ErrorIsLogged() - { - var settings = new RoslynSettings { SonarProjectKey = "projectKey" }; - file.When(x => x.WriteAllText(Arg.Any(), Arg.Any())).Do(x => throw new Exception("Test Exception")); - - testSubject.Update(settings, "any"); - - logger.AssertOutputStrings("[Roslyn Suppressions] Error writing settings for project projectKey. Issues suppressed on the server may not be suppressed in the IDE. Error: Test Exception"); - } - - [TestMethod] - public void Get_ErrorOccuredWhenWritingFile_ErrorIsLoggedAndReturnsNull() - { - file.ReadAllText(GetFilePath("projectKey")).Throws(new Exception("Test Exception")); - - var actual = testSubject.Get("projectKey"); - - logger.AssertOutputStrings("[Roslyn Suppressions] Error loading settings for project projectKey. Issues suppressed on the server will not be suppressed in the IDE. Error: Test Exception"); - actual.Should().BeNull(); - } - - [TestMethod] - public void Update_HasNoIssues_FileWritten() - { - var settings = new RoslynSettings { SonarProjectKey = "projectKey", Suppressions = Enumerable.Empty() }; - - testSubject.Update(settings, "mySolution1"); - - CheckFileWritten(settings, "mySolution1"); - logger.AssertNoOutputMessages(); - } - - [TestMethod] - public void Get_HasNoIssues_ReturnsEmpty() - { - var settings = new RoslynSettings { SonarProjectKey = "projectKey", Suppressions = Enumerable.Empty() }; - MockFileReadAllText(settings); - - var actual = testSubject.Get("projectKey"); - - var issuesGotten = actual.Suppressions.ToList(); - file.Received(1).ReadAllText(GetFilePath("projectKey")); - logger.AssertNoOutputMessages(); - issuesGotten.Count.Should().Be(0); - } - - [TestMethod] - public void Get_FileDoesNotExist_ErrorIsLoggedAndReturnsNull() - { - MockFileSystem(false); - - var actual = testSubject.Get("projectKey"); - - logger.AssertOutputStrings( - "[Roslyn Suppressions] Error loading settings for project projectKey. Issues suppressed on the server will not be suppressed in the IDE. Error: Settings File was not found"); - actual.Should().BeNull(); - } - - [TestMethod] // Regression test for SLVS-2946 - public void SaveAndLoadSettings() - { - string serializedText = null; - CreateSaveAndReloadFile(serializedText); - var projectKey = "projectKey"; - var original = new RoslynSettings - { - SonarProjectKey = projectKey, - Suppressions = - [ - CreateIssue("rule1", "path1", null), // null line number - CreateIssue("RULE2", "PATH2", 111, null, RoslynLanguage.VB) // null hash - ] - }; - - // Act - testSubject.Update(original, "any"); - - var reloaded = testSubject.Get(projectKey); - - reloaded.SonarProjectKey.Should().Be(projectKey); - reloaded.Suppressions.Should().NotBeNull(); - reloaded.Suppressions.Count().Should().Be(2); - - var firstSuppression = reloaded.Suppressions.First(); - firstSuppression.RoslynRuleId.Should().Be("rule1"); - firstSuppression.FilePath.Should().Be("path1"); - firstSuppression.RoslynIssueLine.Should().BeNull(); - firstSuppression.Hash.Should().Be("hash"); - firstSuppression.RoslynLanguage.Should().Be(RoslynLanguage.CSharp); - - var secondSuppression = reloaded.Suppressions.Last(); - secondSuppression.RoslynRuleId.Should().Be("RULE2"); - secondSuppression.FilePath.Should().Be("PATH2"); - secondSuppression.RoslynIssueLine.Should().Be(111); - secondSuppression.Hash.Should().BeNull(); - secondSuppression.RoslynLanguage.Should().Be(RoslynLanguage.VB); - } - - [TestMethod] - public void Delete_FileIsDeleted() - { - testSubject.Delete(SolutionName); - - file.Received(1).Delete(GetFilePath(SolutionName)); - logger.AssertNoOutputMessages(); - } - - [TestMethod] - public void Delete_DeletionFails_Logs() - { - var errorMessage = "deletion failed"; - fileSystem.File.When(x => x.Delete(GetFilePath(SolutionName))).Do(_ => throw new Exception(errorMessage)); - - testSubject.Delete(SolutionName); - - file.Received(1).Delete(GetFilePath(SolutionName)); - logger.AssertPartialOutputStrings(string.Format(Strings.RoslynSettingsFileStorageDeleteError, SolutionName, errorMessage)); - } - - [TestMethod] - public void Delete_CriticalException_ExceptionThrown() - { - fileSystem.File.When(x => x.Delete(GetFilePath(SolutionName))).Do(_ => throw new StackOverflowException()); - - Action act = () => testSubject.Delete(SolutionName); - - act.Should().Throw(); - } - - private void MockFileSystem(bool fileExists = true) - { - file.Exists(Arg.Any()).Returns(fileExists); - fileSystem.File.Returns(file); - - var directoryObject = Substitute.For(); - fileSystem.Directory.Returns(directoryObject); - } - - private void CheckFileWritten(RoslynSettings settings, string solutionName) - { - var expectedFilePath = GetFilePath(solutionName); - var expectedContent = JsonConvert.SerializeObject(settings, Formatting.Indented); - - file.Received(1).WriteAllText(expectedFilePath, expectedContent); - } - - private static string GetFilePath(string projectKey) => RoslynSettingsFileInfo.GetSettingsFilePath(projectKey); - - private void MockFileReadAllText(RoslynSettings settings) => file.ReadAllText(GetFilePath(settings.SonarProjectKey)).Returns(JsonConvert.SerializeObject(settings)); - - private void CreateSaveAndReloadFile(string serializedText) - { - // "Save" the data that was written - file.When(x => x.WriteAllText(Arg.Any(), Arg.Any())) - .Do(callInfo => - { - serializedText = callInfo.ArgAt(1); - }); - - // "Load" the saved data - // Note: using a function here, so the method returns the value of serializedText when the - // method is called, rather than when the mock is created (which would always be null) - file.ReadAllText(Arg.Any()) - .Returns( - _ => - { - serializedText.Should().NotBeNull("Test error: data has not been saved"); - return serializedText; - }); - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/RoslynSettingsFileWatcherTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/RoslynSettingsFileWatcherTests.cs deleted file mode 100644 index b451132f2f..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/RoslynSettingsFileWatcherTests.cs +++ /dev/null @@ -1,204 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.IO; -using System.IO.Abstractions; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarLint.VisualStudio.Roslyn.Suppressions.Settings.Cache; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests -{ - [TestClass] - public class RoslynSettingsFileWatcherTests - { - [TestMethod] - public void Ctor_RegisterToFileWatcherEvents() - { - var fileSystemWatcher = CreateFileSystemWatcher(); - - CreateTestSubject(fileSystemWatcher: fileSystemWatcher.Object); - - fileSystemWatcher.VerifyAdd(x => x.Created += It.IsAny(), Times.Once); - fileSystemWatcher.VerifyAdd(x => x.Deleted += It.IsAny(), Times.Once); - fileSystemWatcher.VerifyAdd(x => x.Changed += It.IsAny(), Times.Once); - fileSystemWatcher.VerifySet(x=> x.Filter = "*.json", Times.Once); - fileSystemWatcher.VerifySet(x=> x.EnableRaisingEvents = true, Times.Once); - - fileSystemWatcher.VerifyNoOtherCalls(); - } - - [TestMethod] - public void Dispose_UnregisterFromFileWatcherEvents() - { - var fileSystemWatcher = CreateFileSystemWatcher(); - - var testSubject = CreateTestSubject(fileSystemWatcher: fileSystemWatcher.Object); - - fileSystemWatcher.VerifyRemove(x => x.Created -= It.IsAny(), Times.Never); - fileSystemWatcher.VerifyRemove(x => x.Deleted -= It.IsAny(), Times.Never); - fileSystemWatcher.VerifyRemove(x => x.Changed -= It.IsAny(), Times.Never); - fileSystemWatcher.Verify(x => x.Dispose(), Times.Never); - - testSubject.Dispose(); - - fileSystemWatcher.VerifyRemove(x => x.Created -= It.IsAny(), Times.Once); - fileSystemWatcher.VerifyRemove(x => x.Deleted -= It.IsAny(), Times.Once); - fileSystemWatcher.VerifyRemove(x => x.Changed -= It.IsAny(), Times.Once); - fileSystemWatcher.Verify(x=> x.Dispose(), Times.Once); - } - - [TestMethod] - [Description("It is a deliberate design choice to let the caller handle initialization exceptions.")] - public void Ctor_FailedToCreateFileSystemWatcher_ExceptionsNotHandled() - { - var fileSystemWatcher = new Mock(); - fileSystemWatcher - .SetupSet(x => x.EnableRaisingEvents = true) - .Throws(new NotImplementedException("some exception")); - - Action act = () => CreateTestSubject(fileSystemWatcher: fileSystemWatcher.Object); - - act.Should().Throw().And.Message.Should().Be("some exception"); - } - - [TestMethod] - [DataRow(WatcherChangeTypes.Created)] - [DataRow(WatcherChangeTypes.Deleted)] - [DataRow(WatcherChangeTypes.Changed)] - public void OnFileSystemChanges_SettingsKeyNotFound_CacheNotInvalidated(WatcherChangeTypes changeType) - { - var fileSystemWatcher = new Mock(); - var settingsCache = new Mock(); - - CreateTestSubject(settingsCache.Object, fileSystemWatcher.Object); - - settingsCache.Invocations.Count.Should().Be(0); - - RaiseFileSystemEvent(fileSystemWatcher, changeType, null); - - settingsCache.Invocations.Count.Should().Be(0); - } - - [TestMethod] - [DataRow(WatcherChangeTypes.Created)] - [DataRow(WatcherChangeTypes.Deleted)] - [DataRow(WatcherChangeTypes.Changed)] - public void OnFileSystemChanges_CacheInvalidated(WatcherChangeTypes changeType) - { - var fileSystemWatcher = new Mock(); - var settingsCache = new Mock(); - - CreateTestSubject(settingsCache.Object, fileSystemWatcher.Object); - - settingsCache.Invocations.Count.Should().Be(0); - - RaiseFileSystemEvent(fileSystemWatcher, changeType, "c:\\a\\b\\c\\some file.txt"); - - settingsCache.Verify(x=> x.Invalidate("some file"), Times.Once); - settingsCache.VerifyNoOtherCalls(); - } - - [TestMethod] - [DataRow(WatcherChangeTypes.Created)] - [DataRow(WatcherChangeTypes.Deleted)] - [DataRow(WatcherChangeTypes.Changed)] - public void OnFileSystemChanges_ExceptionInvalidatingCache_ExceptionHandled(WatcherChangeTypes changeType) - { - var fileSystemWatcher = new Mock(); - - var settingsCache = new Mock(); - settingsCache - .Setup(x => x.Invalidate("some file")) - .Throws(new NotImplementedException("some exception")); - - var logger = new TestLogger(); - - CreateTestSubject(settingsCache.Object, fileSystemWatcher.Object, logger: logger); - - settingsCache.Invocations.Count.Should().Be(0); - - Action act = () => RaiseFileSystemEvent(fileSystemWatcher, changeType, "c:\\a\\b\\c\\some file.txt"); - - act.Should().NotThrow(); - logger.AssertPartialOutputStringExists("some exception"); - } - - private static Mock CreateFileSystemWatcher() - { - var fileSystemWatcher = new Mock(); - - fileSystemWatcher.SetupAdd(x => x.Created += null); - fileSystemWatcher.SetupAdd(x => x.Deleted += null); - fileSystemWatcher.SetupAdd(x => x.Changed += null); - - fileSystemWatcher.SetupRemove(x => x.Created -= null); - fileSystemWatcher.SetupRemove(x => x.Deleted -= null); - fileSystemWatcher.SetupRemove(x => x.Changed -= null); - - return fileSystemWatcher; - } - - private static void RaiseFileSystemEvent(Mock fileSystemWatcher, WatcherChangeTypes changeType, string fileName) - { - var eventArgs = new FileSystemEventArgs(changeType, "some dir", fileName); - - switch (changeType) - { - case WatcherChangeTypes.Created: - fileSystemWatcher.Raise(x => x.Created += null, eventArgs); - break; - case WatcherChangeTypes.Deleted: - fileSystemWatcher.Raise(x => x.Deleted += null, eventArgs); - break; - case WatcherChangeTypes.Changed: - fileSystemWatcher.Raise(x => x.Changed += null, eventArgs); - break; - default: - throw new ArgumentOutOfRangeException(nameof(changeType), changeType, null); - } - } - - private static SuppressedIssuesFileWatcher CreateTestSubject(ISettingsCache settingsCache = null, - IFileSystemWatcher fileSystemWatcher = null, - IFileSystem fileSystem = null, - ILogger logger = null) - { - fileSystemWatcher ??= Mock.Of(); - fileSystem ??= Mock.Of(); - logger ??= Mock.Of(); - - Mock.Get(fileSystem) - .Setup(x => x.FileSystemWatcher.FromPath(RoslynSettingsFileInfo.Directory)) - .Returns(fileSystemWatcher); - - Mock.Get(fileSystem).Setup(x => x.Directory.CreateDirectory(RoslynSettingsFileInfo.Directory)); - - settingsCache ??= Mock.Of(); - - return new SuppressedIssuesFileWatcher(settingsCache, logger, fileSystem); - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/Settings.Cache/SettingsCacheTest.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/Settings.Cache/SettingsCacheTest.cs deleted file mode 100644 index ea855af74c..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/Settings.Cache/SettingsCacheTest.cs +++ /dev/null @@ -1,165 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Concurrent; -using System.Globalization; -using System.Linq; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using SonarLint.VisualStudio.TestInfrastructure.Helpers; -using SonarLint.VisualStudio.Roslyn.Suppressions.Settings.Cache; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.Settings.Cache -{ - [TestClass] - public class SettingsCacheTest - { - [TestMethod] - public void GetSettings_SettingNotInCache_SettingsReadFromFile() - { - var settings = new RoslynSettings { SonarProjectKey = "my project" }; - var cacheObject = CreateEmptyCacheObject(); - - var fileStorage = new Mock(); - fileStorage.Setup(fs => fs.Get("settingsKey")).Returns(settings); - - var testSubject = CreateTestSubject(fileStorage, cacheObject); - var actual = testSubject.GetSettings("settingsKey"); - - fileStorage.Verify(fs => fs.Get("settingsKey"), Times.Once); - cacheObject.ContainsKey("settingsKey").Should().BeTrue(); - cacheObject["settingsKey"].Should().BeSameAs(settings); - actual.Should().BeSameAs(settings); - } - - [DataRow("settingskey", "settingskey")]//Testing lower case - [DataRow("settingsKEY", "settingskey")]//Testing upper case - [DataRow("III", "iii")]//Testing upper case with invariant culture - [TestMethod] - public void GetSettings_SettingInCache_SettingsReadFromCache(string settingsKey, string normalisedKey) - { - //This is to make sure normalising the keys done correctly with invariant culture - //https://en.wikipedia.org/wiki/Dotted_and_dotless_I - using var scope = new TurkishCultureScope(); - - var settings = new RoslynSettings { SonarProjectKey = "my project" }; - - var cacheObject = CreatePopulatedCacheObject(normalisedKey, settings); - - var fileStorage = new Mock(); - - var testSubject = CreateTestSubject(fileStorage, cacheObject); - var actual = testSubject.GetSettings(settingsKey); - - fileStorage.Verify(fs => fs.Get(It.IsAny()), Times.Never); - actual.Should().BeSameAs(settings); - - } - - [TestMethod] - public void GetSettings_SettingNotInCacheOrFile_SettingsEmpty() - { - var fileStorage = new Mock(); - - var testSubject = CreateTestSubject(fileStorage); - var actual = testSubject.GetSettings("settingsKey"); - - fileStorage.Verify(fs => fs.Get("settingsKey"), Times.Once); - CheckSettingsAreEmpty(actual); - } - - [TestMethod] - public void GetSettings_DifferentSettingInCache_SettingsEmpty() - { - var settings = new RoslynSettings { SonarProjectKey = "my project" }; - - var cacheObject = CreatePopulatedCacheObject("differentkey", settings); - - var fileStorage = new Mock(); - - var testSubject = CreateTestSubject(fileStorage, cacheObject); - var actual = testSubject.GetSettings("settingsKey"); - - fileStorage.Verify(fs => fs.Get("settingsKey"), Times.Once); - CheckSettingsAreEmpty(actual); - } - - [DataRow("settingskey", "settingskey")]//Testing lower case - [DataRow("settingsKEY", "settingskey")]//Testing upper case - [DataRow("III","iii")]//Testing upper case with invariant culture - [TestMethod] - public void Invalidate_SettingInCache_SettingsRemovedFromCache(string settingsKey, string normalisedKey) - { - //This is to make sure normalising the keys done correctly with invariant culture - //https://en.wikipedia.org/wiki/Dotted_and_dotless_I - using var scope = new TurkishCultureScope(); - - var cacheObject = CreatePopulatedCacheObject(normalisedKey, new RoslynSettings()); - cacheObject.ContainsKey(normalisedKey).Should().BeTrue("Test setup error: cache was not pre-populated correctly"); - - var testSubject = CreateTestSubject(settingsCollection: cacheObject); - testSubject.Invalidate(settingsKey); - - cacheObject.ContainsKey(normalisedKey).Should().BeFalse(); - - } - [TestMethod] - public void Invalidate_SettingNotInCache_NoErrorThrown() - { - var testSubject = CreateTestSubject(); - - testSubject.Invalidate("settingsKey"); - } - - private ConcurrentDictionary CreatePopulatedCacheObject(string settingsKey, RoslynSettings settings) - { - var cacheObject = CreateEmptyCacheObject(); - cacheObject.AddOrUpdate(settingsKey, settings, (x, y) => settings); - - return cacheObject; - } - - - private SettingsCache CreateTestSubject(Mock fileStorage = null, ConcurrentDictionary settingsCollection = null) - { - fileStorage = fileStorage ?? new Mock(); - settingsCollection = settingsCollection ?? new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - - return new SettingsCache(fileStorage.Object, settingsCollection); - } - - private ConcurrentDictionary CreateEmptyCacheObject() - { - return new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - } - - private static void CheckSettingsAreEmpty(RoslynSettings settings) - { - settings.Should().BeSameAs(RoslynSettings.Empty); - - settings.SonarProjectKey.Should().BeNull(); - settings.Suppressions.Should().NotBeNull(); - settings.Suppressions.Count().Should().Be(0); - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SonarDiagnosticSuppressorTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SonarDiagnosticSuppressorTests.cs deleted file mode 100644 index d1c377e21a..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SonarDiagnosticSuppressorTests.cs +++ /dev/null @@ -1,244 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Immutable; -using System.Linq; -using FluentAssertions; -using Microsoft.CodeAnalysis; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using SonarLint.VisualStudio.Core; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests -{ - [TestClass] - public class SonarDiagnosticSuppressorTests - { - [TestMethod] - public void Suppressor_ReturnsExpectedSuppressions() - { - var testSubject = CreateTestSubject(); - - var actual = testSubject.SupportedSuppressions; - - // Note: ImmutableArray<> is a value type so we can't use ReferenceEquals or ".Should().BeSameAs(...)" - // to check if the object is the same. Howevere, "Equals" is overloaded to test the underlying arrays are - // the same instance. - // See https://docs.microsoft.com/en-us/dotnet/api/system.collections.immutable.immutablearray-1.equals?view=net-6.0 - actual.Equals(SupportedSuppressionsBuilder.Instance.Descriptors).Should().BeTrue(); - } - - [TestMethod] - public void Suppressors_AllSuppressorsReturnSameInstances() - { - // Perf - check every instance reuses the same set of descriptors - var supported1 = CreateTestSubject().SupportedSuppressions; - var supported2 = CreateTestSubject().SupportedSuppressions; - - // Have to use "Equals" to test identityt equality - see above test. - supported1.Equals(supported2).Should().BeTrue(); - } - - [TestMethod] - public void GetSuppressionsIsNotCalled_ContainerNotInitialized() - { - var createContainer = new Mock>(); - - var testSubject = CreateTestSubject(createContainer.Object); - - createContainer.Invocations.Count.Should().Be(0); - - var supportedSuppressions = testSubject.SupportedSuppressions; - - supportedSuppressions.Should().NotBeEmpty(); - - createContainer.Invocations.Count.Should().Be(0); - } - - [TestMethod] - public void GetSuppressions_IsNotInConnectedMode_ContainerNotInitialized() - { - var createContainer = new Mock>(); - - var suppressionContext = CreateSuppressionContext(null); - var reportedDiagnostics = CreateReportedDiagnostics(); - - var testSubject = CreateTestSubject(createContainer.Object); - - var suppressions = testSubject.GetSuppressions(reportedDiagnostics, suppressionContext.Object); - - suppressions.Count().Should().Be(0); - - createContainer.Invocations.Count.Should().Be(0); - - suppressionContext.VerifyGet(c => c.IsInConnectedMode, Times.Once); - } - - [TestMethod] - public void GetSuppressions_IsInConnectedMode_ContainerInitialized() - { - var suppressionContext = CreateSuppressionContext("sonarKey"); - var suppressionChecker = CreateSuppressionChecker("sonarKey"); - - var createContainer = new Mock>(); - createContainer.Setup(x => x()).Returns(CreateContainer(suppressionChecker)); - - var reportedDiagnostics = CreateReportedDiagnostics(); - - var testSubject = CreateTestSubject(createContainer.Object); - - var suppressions = testSubject.GetSuppressions(reportedDiagnostics, suppressionContext.Object); - - suppressions.Count().Should().Be(0); - - createContainer.Invocations.Count.Should().Be(1); - } - - [TestMethod] - public void GetSuppressions_IsNotInConnectedMode_ShouldReturnEmpty() - { - var suppressionContext = CreateSuppressionContext(null); - var reportedDiagnostics = CreateReportedDiagnostics(); - - var testSubject = CreateTestSubject(); - - var suppressions = testSubject.GetSuppressions(reportedDiagnostics, suppressionContext.Object); - - suppressions.Count().Should().Be(0); - } - - [TestMethod] - public void GetSuppressions_HasSuppressedDiagnostic_ShouldReturnSuppressions() - { - var suppressionContext = CreateSuppressionContext("sonarKey"); - var diag = CreateDiagnostic("S100"); - - var suppressionChecker = CreateSuppressionChecker("sonarKey", diag); - var container = CreateContainer(suppressionChecker); - - var reportedDiagnostics = CreateReportedDiagnostics(diag); - - var testSubject = CreateTestSubject(container); - - var suppressions = testSubject.GetSuppressions(reportedDiagnostics, suppressionContext.Object).ToList(); - - suppressions.Count().Should().Be(1); - suppressions[0].Descriptor.SuppressedDiagnosticId.Should().Be("S100"); - } - - [TestMethod] - public void GetSuppressions_HasMixedSuppressedDiagnostic_ShouldReturnOnlySuppressed() - { - - var suppressionContext = CreateSuppressionContext("sonarKey"); - var diag1 = CreateDiagnostic("S100"); - var diag2 = CreateDiagnostic("S101"); - var diag3 = CreateDiagnostic("S103"); - - var suppressionChecker = CreateSuppressionChecker("sonarKey", diag1, diag2); - var container = CreateContainer(suppressionChecker); - - var reportedDiagnostics = CreateReportedDiagnostics(diag1, diag2, diag3); - - var testSubject = CreateTestSubject(container); - - var suppressions = testSubject.GetSuppressions(reportedDiagnostics, suppressionContext.Object).ToList(); - - suppressions.Count().Should().Be(2); - suppressions[0].Descriptor.SuppressedDiagnosticId.Should().Be("S100"); - suppressions[1].Descriptor.SuppressedDiagnosticId.Should().Be("S101"); - } - - [TestMethod] - public void GetSuppressions_HasNotSuppressedDiagnostic_ShouldReturnEmpty() - { - var suppressionContext = CreateSuppressionContext("sonarKey"); - var diag = CreateDiagnostic("S100"); - - var reportedDiagnostics = CreateReportedDiagnostics(diag); - - var testSubject = CreateTestSubject(); - - var suppressions = testSubject.GetSuppressions(reportedDiagnostics, suppressionContext.Object).ToList(); - - suppressions.Count().Should().Be(0); - } - - private ISuppressionChecker CreateSuppressionChecker(string settingsKey, params Diagnostic[] diagnostics) - { - var suppressionChecker = new Mock(); - suppressionChecker.Setup(sc => sc.IsSuppressed(It.IsAny(), It.IsAny())).Returns(false); - - foreach (var diagnostic in diagnostics) - { - suppressionChecker.Setup(sc => sc.IsSuppressed(diagnostic, settingsKey)).Returns(true); - } - - return suppressionChecker.Object; - } - - private Mock CreateSuppressionContext(string settingsKey) - { - var context = new Mock(); - context.SetupGet(c => c.SettingsKey).Returns(settingsKey); - context.SetupGet(c => c.IsInConnectedMode).Returns(settingsKey != null); - - return context; - } - - private Diagnostic CreateDiagnostic(string id) - { - var diagnostic = new Mock(); - diagnostic.SetupGet(d => d.Id).Returns(id); - - return diagnostic.Object; - } - - private ImmutableArray CreateReportedDiagnostics(params Diagnostic[] diagnostics) - { - return diagnostics.ToImmutableArray(); - } - - private SonarDiagnosticSuppressor CreateTestSubject(IContainer container) => - CreateTestSubject(() => container); - - private SonarDiagnosticSuppressor CreateTestSubject(Func createContainer = null) - { - createContainer ??= () => CreateContainer(); - - return new SonarDiagnosticSuppressor(createContainer); - } - - private IContainer CreateContainer(ISuppressionChecker suppressionChecker = null, ILogger logger = null) - { - suppressionChecker ??= Mock.Of(); - logger ??= Mock.Of(); - - var container = new Mock(); - container.SetupGet(c => c.SuppressionChecker).Returns(suppressionChecker); - container.SetupGet(c => c.Logger).Returns(logger); - - return container.Object; - } - - - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SupportedSuppressionBuilderTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SupportedSuppressionBuilderTests.cs deleted file mode 100644 index c8ca6d36f2..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SupportedSuppressionBuilderTests.cs +++ /dev/null @@ -1,71 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Linq; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests -{ - [TestClass] - public class SupportedSuppressionBuilderTests - { - [TestMethod] - public void SupportedSuppressors_Instance_IsSingleton() - { - SupportedSuppressionsBuilder.Instance.Should().BeSameAs(SupportedSuppressionsBuilder.Instance); - } - - [TestMethod] - public void SupportedSuppressors_SuppressorsCreated() - { - var result = SupportedSuppressionsBuilder.Instance.Descriptors; - - // The exact number will vary every time a new version of either C# or VB analyzer is released - // so we'll check a ball-park figure - result.Count().Should().BeGreaterThan(400); - } - - [TestMethod] - public void SupportedSuppressors_ItemsAreNotNull() - { - var result = SupportedSuppressionsBuilder.Instance.Descriptors; - - result.Any(x => x == null).Should().BeFalse(); - } - - [TestMethod] - public void SupportedSuppressions_NoDuplicates() - { - var result = SupportedSuppressionsBuilder.Instance.Descriptors; - - var distinctIdCount = result.Select(x => x.Id).Distinct().Count(); - result.Length.Should().Be(distinctIdCount); - } - - [TestMethod] - public void SupportedSuppressors_NoUtilityAnalyzers() - { - var result = SupportedSuppressionsBuilder.Instance.Descriptors; - - result.Any(x => x.Id.StartsWith("S9999")).Should().BeFalse(); - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SuppressedIssueTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SuppressedIssueTests.cs deleted file mode 100644 index 2e3dd89712..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SuppressedIssueTests.cs +++ /dev/null @@ -1,96 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests; - -[TestClass] -public class SuppressedIssueTests -{ - [TestMethod] - public void AreSame_ReturnsTrueIfPropertiesHaveSameValue() - { - var suppressedIssue1 = TestHelper.CreateIssue(); - var suppressedIssue2 = TestHelper.CreateIssue(); - - suppressedIssue1.AreSame(suppressedIssue2).Should().BeTrue(); - } - - [TestMethod] - public void AreSame_ReturnsFalseIfRuleIdIsDifferent() - { - var suppressedIssue1 = TestHelper.CreateIssue(); - var suppressedIssue2 = TestHelper.CreateIssue(ruleId: "S666"); - - suppressedIssue1.AreSame(suppressedIssue2).Should().BeFalse(); - } - - [TestMethod] - public void AreSame_ReturnsFalseIfFilePathIsDifferent() - { - var suppressedIssue1 = TestHelper.CreateIssue(); - var suppressedIssue2 = TestHelper.CreateIssue(path: "differentPath"); - - suppressedIssue1.AreSame(suppressedIssue2).Should().BeFalse(); - } - - [TestMethod] - public void AreSame_ReturnsFalseIfLineIsDifferent() - { - var suppressedIssue1 = TestHelper.CreateIssue(); - var suppressedIssue2 = TestHelper.CreateIssue(line: suppressedIssue1.RoslynIssueLine + 1); - - suppressedIssue1.AreSame(suppressedIssue2).Should().BeFalse(); - } - - [TestMethod] - public void AreSame_ReturnsFalseIfHashIsDifferent() - { - var suppressedIssue1 = TestHelper.CreateIssue(); - var suppressedIssue2 = TestHelper.CreateIssue(hash: "differentHash"); - - suppressedIssue1.AreSame(suppressedIssue2).Should().BeFalse(); - } - - [TestMethod] - public void AreSame_ReturnsFalseIfLanguageIsDifferent() - { - var suppressedIssue1 = TestHelper.CreateIssue(); - var suppressedIssue2 = TestHelper.CreateIssue(language: RoslynLanguage.VB); - - suppressedIssue1.AreSame(suppressedIssue2).Should().BeFalse(); - } - - [TestMethod] - public void AreSame_ReturnsFalseIfIssueServerKeyIsDifferent() - { - var suppressedIssue1 = TestHelper.CreateIssue(); - var suppressedIssue2 = TestHelper.CreateIssue(issueServerKey: "key6"); - - suppressedIssue1.AreSame(suppressedIssue2).Should().BeFalse(); - } - - [TestMethod] - public void AreSame_ReturnsFalseIfSuppressedIssueIsNull() - { - var suppressedIssue1 = TestHelper.CreateIssue(); - - suppressedIssue1.AreSame(null).Should().BeFalse(); - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SuppressionCheckerTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SuppressionCheckerTests.cs deleted file mode 100644 index 21e6a3d82c..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SuppressionCheckerTests.cs +++ /dev/null @@ -1,401 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Generic; -using System.IO; -using FluentAssertions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using SonarLint.VisualStudio.Roslyn.Suppressions.Settings.Cache; -using SonarQube.Client; -using static SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.TestHelper; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests -{ - [TestClass] - public class SuppressionCheckerTests - { - // Cache that will throw if it is called - private static readonly ISettingsCache ThrowingSettingsCache = new Mock(MockBehavior.Strict).Object; - - private readonly Diagnostic ValidInSourceDiagnostic = CreateDiagnostic("any", - CreateSourceFileLocation("c:\\any.txt")); - - [TestMethod] - public void IsSuppressed_NonSourceLocation_ReturnsFalse() - { - // Perf - should early-out for non-source location - var testSubject = CreateTestSubject(ThrowingSettingsCache); - - var nonSourceDiagnostic = CreateDiagnostic("any", CreateNonSourceLocation()); - - // Act - var actual = testSubject.IsSuppressed(nonSourceDiagnostic, "any"); - - actual.Should().BeFalse(); - } - - [TestMethod] - public void IsSuppressed_NullDiagnosticLocation_ReturnsFalse() - { - // Perf - should early-out for null diagnostic location - var testSubject = CreateTestSubject(ThrowingSettingsCache); - - var nullLocationDiagnostic = CreateDiagnostic("any", null); - - // Act - var actual = testSubject.IsSuppressed(nullLocationDiagnostic, "any"); - - actual.Should().BeFalse(); - } - - - [TestMethod] - public void IsSuppressed_NullSettings_ReturnsFalse() - { - var cache = CreateSettingsCache("settingsKey1", null); - var testSubject = CreateTestSubject(cache.Object); - - // Act - var actual = testSubject.IsSuppressed(ValidInSourceDiagnostic, "settingsKey1"); - - actual.Should().Be(false); - CheckGetSettingsCalled(cache, "settingsKey1"); - } - - [TestMethod] - public void IsSuppressed_EmptySettings_ReturnsFalse() - { - var cache = CreateSettingsCache("settingsKey1", new SuppressedIssue[0]); - - var testSubject = CreateTestSubject(cache.Object); - - // Act - var actual = testSubject.IsSuppressed(ValidInSourceDiagnostic, "settingsKey1"); - - actual.Should().Be(false); - CheckGetSettingsCalled(cache, "settingsKey1"); - } - - [TestMethod] - public void IsSuppressed_HasIssues_NoMatches_ReturnsFalse() - { - var diagnostic = CreateDiagnostic("S111", CreateSourceFileLocation("c:\\myfile.cs")); - - var suppressedIssues = new SuppressedIssue[] - { - CreateIssue("S111", "wrongFile1.txt") - }; - - var cache = CreateSettingsCache("settingsKey", suppressedIssues); - var checksumCalculator = CreateChecksumCalculator("any"); - var testSubject = CreateTestSubject(cache.Object, checksumCalculator.Object); - - // Act - var actual = testSubject.IsSuppressed(diagnostic, "settingsKey"); - - actual.Should().BeFalse(); - CheckGetSettingsCalled(cache, "settingsKey"); - } - - [TestMethod] - public void IsSuppressed_HasIssues_Matches_ReturnsTrue() - { - var location = CreateSourceFileLocation(DiagFileName, DiagFileText, DiagSelectedText); - var diagnostic = CreateDiagnostic(DiagRuleId, location); - - var suppressedIssues = new[] - { - CreateIssueFromDiagnostic(diagnostic) - }; - - var checksumCalculator = CreateChecksumCalculator(DiagHash); - - var cache = CreateSettingsCache("settingsKey", suppressedIssues); - var testSubject = CreateTestSubject(cache.Object, checksumCalculator.Object); - - // Act - var actual = testSubject.IsSuppressed(diagnostic, "settingsKey"); - - actual.Should().BeTrue(); - CheckGetSettingsCalled(cache, "settingsKey"); - } - - [TestMethod] - public void IsMatch_FilesAreDifferent_ReturnsFalse() - { - var checksumCalculator = CreateChecksumCalculator("hash"); - - var diag = CreateDiagnostic("S999", CreateSourceFileLocation("c:\\diagnosticFileName.cs", "all text", "all")); - // Sanity check the diagnostic location is on the expected line - diag.Location.GetLineSpan().StartLinePosition.Line.Should().Be(0, "Test setup error"); - - var issue = CreateIssue("S999", "issueFile.cs", 1, "hash"); - - SuppressionChecker.IsMatch(diag, issue, checksumCalculator.Object) - .Should().BeFalse(); - - checksumCalculator.Invocations.Count.Should().Be(0); - } - - [TestMethod] - public void IsMatch_RuleIdsAreDifferent_ReturnsFalse() - { - var checksumCalculator = CreateChecksumCalculator("hash"); - - var diag = CreateDiagnostic("S111", CreateSourceFileLocation("c:\\file.cs", "all text", "all")); - // Sanity check the diagnostic location is on the expected line - diag.Location.GetLineSpan().StartLinePosition.Line.Should().Be(0, "Test setup error"); - - var issue = CreateIssue("S999", "c\\file.cs", 1, "hash"); - - SuppressionChecker.IsMatch(diag, issue, checksumCalculator.Object) - .Should().BeFalse(); - - checksumCalculator.Invocations.Count.Should().Be(0); - - } - - #region Matching tests - - // Constants used in for the diagnostic to match against in IsMatch_ReturnsExpectedValue - // They are defined as constants here so we can use them in the [DataRow]s for the test. - private const string DiagFileName = "c:\\diag.txt"; - private const string MatchingServerFileName = "diag.txt"; - private const string DiagRuleId = "S999"; - private const string DiagHash = "diag hash"; - - private const string DiagFileText = "0\n1\n2\n3 text \n\n"; - private const string DiagSelectedText = "text"; - private const int DiagRoslynLineNumber = 3; // the 0-base line in which the word "text" appears - - [TestMethod] - [DataRow(MatchingServerFileName, DiagRuleId, DiagRoslynLineNumber, DiagHash, true)] - [DataRow("wrong file name.cs", DiagRuleId, DiagRoslynLineNumber, DiagHash, false)] - [DataRow(MatchingServerFileName, "wrong rule id", DiagRoslynLineNumber, DiagHash, false)] - [DataRow(MatchingServerFileName, DiagRuleId, 999, "wrong hash", false)] // wrong line, wrong hash -> false - [DataRow(MatchingServerFileName, DiagRuleId, DiagRoslynLineNumber, "wrong hash", true)] // right line, wrong hash -> true - [DataRow(MatchingServerFileName, DiagRuleId, 888, DiagHash, true)] // wrong line, right hash -> true - - // Special cases - [DataRow("DIAG.TXT", DiagRuleId, DiagRoslynLineNumber, DiagHash, true)] // case-insensitive - [DataRow(MatchingServerFileName, "s999", DiagRoslynLineNumber, DiagHash, true)] // case-insensitive - public void IsMatch_ReturnsExpectedValue(string issueFile, string issueRuleId, int issueLine, - string issueHash, bool expected) - { - // The diagnostic is the same in every case - var checksumCalculator = CreateChecksumCalculator(DiagHash); - var diag = CreateDiagnostic(DiagRuleId, CreateSourceFileLocation(DiagFileName, DiagFileText, DiagSelectedText)); - // Sanity check the diagnostic location is on the expected line - diag.Location.GetLineSpan().StartLinePosition.Line.Should().Be(DiagRoslynLineNumber, "Test setup error"); - - var issue = CreateIssue(issueRuleId, issueFile, issueLine, issueHash); - - SuppressionChecker.IsMatch(diag, issue, checksumCalculator.Object) - .Should().Be(expected); - } - - [TestMethod] - [DataRow(0, 0, true)] - [DataRow(1, 0, false)] - [DataRow(1, 1, false)] - [DataRow(2, 0, false)] - [DataRow(2, 2, false)] - public void IsMatch_IsRoslynFileLevelIssue_MatchesSonarFileLevelIssue(int roslynStartPosition, int length, - bool expected) - { - const string text = "000\n111\n222\n"; - var selectedSpan = new TextSpan(roslynStartPosition, length); - var syntaxTree = Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.ParseText(text, path: MatchingWellKnownLocalPath); - var location = Location.Create(syntaxTree, selectedSpan); - var diagnostic = CreateDiagnostic("id", location); - - var sonarFileLevelIssue = CreateIssue(ruleId: "id", path: WellKnownRelativeServerPath, hash: "hash", line: null); - - var actual = SuppressionChecker.IsMatch(diagnostic, sonarFileLevelIssue, Mock.Of()); - - actual.Should().Be(expected); - } - - [TestMethod] - public void IsMatch_RoslynFileLevelIssue_SonarNonFileLevelIssue_DoNotMatch() - { - const string text = "000\n111\n222\n"; - var selectedSpan = new TextSpan(0, 0); - var syntaxTree = Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.ParseText(text, path: MatchingWellKnownLocalPath); - var location = Location.Create(syntaxTree, selectedSpan); - var diagnostic = CreateDiagnostic("id", location); - - var sonarNonFileLevelIssue = CreateIssue(ruleId: "id", path: WellKnownRelativeServerPath, hash: "hash", line: 2); - - var actual = SuppressionChecker.IsMatch(diagnostic, sonarNonFileLevelIssue, Mock.Of()); - - actual.Should().BeFalse(); - } - - private const string WellKnownRelativeServerPath = "file.txt"; - private const string MatchingWellKnownLocalPath = "c:\\" + WellKnownRelativeServerPath; - - [TestMethod] - [DataRow(@"same.txt", @"c:\same.txt", true)] - [DataRow(@"SAME.TXT", @"c:\same.txt", true)] - [DataRow(@"differentExt.123", @"d:\differentExt.999", false)] - [DataRow(@"partial\file.cs", @"c:\aaa\partial\file.cs", true)] - [DataRow(@"aaa\partial\file.cs", @"x:\partial\file.cs", false)] - public void IsMatch_IsSameFile(string serverIssueFile, string diagFile, bool expected) - { - var checksumCalculator = CreateChecksumCalculator("a hash that won't match"); - - var diag = CreateDiagnostic(DiagRuleId, CreateSourceFileLocation(diagFile, DiagFileText, DiagSelectedText)); - - var issue = CreateIssueFromDiagnostic(diag, overrideFilePath: serverIssueFile); - - SuppressionChecker.IsMatch(diag, issue, checksumCalculator.Object) - .Should().Be(expected); - } - - #endregion // Matching tests - - private void CheckGetSettingsCalled(Mock cache, string expectedSettingsKey) => - cache.Verify(x => x.GetSettings(expectedSettingsKey), Times.Once()); - - private static SuppressionChecker CreateTestSubject(ISettingsCache cache = null, - IChecksumCalculator checksumCalculator = null) - { - cache ??= CreateSettingsCache("any", null).Object; - checksumCalculator ??= CreateChecksumCalculator("any").Object; - - return new SuppressionChecker(cache, checksumCalculator); - } - - private static Mock CreateSettingsCache(string settingsKey, IEnumerable suppressedIssues) - { - var cache = new Mock(); - var settings = new RoslynSettings { Suppressions = suppressedIssues }; - cache.Setup(x => x.GetSettings(settingsKey)).Returns(settings); - return cache; - } - - private static Mock CreateChecksumCalculator(string hashToReturn) - { - var calculator = new Mock(); - calculator.Setup(x => x.Calculate(It.IsAny())) - .Returns(hashToReturn); - - return calculator; - } - - /// - /// Returns an issue with the same values as the supplied diagnostic. - /// - /// - /// The issue will match the diagnostic, unless the file path is overridden. - /// The diagnostic must have a valid location and source tree. - /// - private static SuppressedIssue CreateIssueFromDiagnostic(Diagnostic diagnostic, - string overrideFilePath = null) - { - // The issue will match because the line number is the same (so the hash is irrelevant) - var sonarLine = diagnostic.Location.GetLineSpan().EndLinePosition.Line; - - var serverIssuePath = overrideFilePath ?? GetMatchingRelativePath(diagnostic.Location.SourceTree.FilePath); - - var issueHash = Guid.NewGuid().ToString(); - - return CreateIssue(diagnostic.Id, serverIssuePath, sonarLine, issueHash); - } - - private static string GetMatchingRelativePath(string absolutePath) - { - // The matching server should not be rooted, so we'll strip off the - // root and return the rest of the path. The product code should be - // considered that to be a valid match for the original full path. - if (!Path.IsPathRooted(absolutePath)) - { - throw new ArgumentException(nameof(absolutePath), $"Test setup error: path should be rooted. Actual: {absolutePath}"); - } - var root = Path.GetPathRoot(absolutePath); - return absolutePath.Substring(root.Length); - } - - #region Diagnostic helper methods - - private static Location CreateNonSourceLocation() - { - var nonSourceLocation = Location.Create("dummyFilePath.cs", new TextSpan(), new LinePositionSpan()); - nonSourceLocation.IsInSource.Should().BeFalse(); - nonSourceLocation.Kind.Should().NotBe(LocationKind.SourceFile); - return nonSourceLocation; - } - - /// - /// Returns a Location that is in a source file, backed by a valid syntax tree. - /// - /// The contents of the entire file (optional) - /// The specific text in the file the location points to (optional) - private static Location CreateSourceFileLocation(string filePath, - string fileText = "any", string selectedText = "any") - { - // Mocking source location so that it appears to be backed by a real syntax tree, with - // the required "GetLineSpan()", "GetText().Lines" etc methods working correctly is hard. - - // The simplest thing to do is create a real C# syntax tree. It doesn't matter if the - // contents of the fake file are not valid C# - the parser will still produce a valid - // document. - - var syntaxTree = Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.ParseText(fileText, path: filePath); - var selectedSpan = CreateSpan(fileText, selectedText); - var location = Location.Create(syntaxTree, selectedSpan); - - // Sanity checks that the objects are set up as we expected - location.IsInSource.Should().BeTrue("Test setup error: location should be in source"); - location.Kind.Should().Be(LocationKind.SourceFile, "Test setup error: LocationKind should be SourceFile"); - syntaxTree.FilePath.Should().Be(filePath, "Test setup error: syntaxTree FilePath is not set correctly"); - var lineSpan = location.GetLineSpan(); - lineSpan.IsValid.Should().BeTrue("Test setup error: lineSpan is not valid"); - var lineText = syntaxTree.GetText().Lines[lineSpan.EndLinePosition.Line].ToString(); - lineText.Should().Contain(selectedText, "Test setup error: the fake location/syntax tree is not correctly constructed"); - - return location; - } - - private static TextSpan CreateSpan(string documentText, string selectedText) - { - var start = documentText.IndexOf(selectedText); - start.Should().BeGreaterThan(-1, "Test setup error: selected text is not in the document text"); - return new TextSpan(start, selectedText.Length); - } - - private static Diagnostic CreateDiagnostic(string ruleId, Location loc) - { - var diagnostic = new Mock(); - - diagnostic.Setup(x => x.Id).Returns(ruleId); - diagnostic.Setup(x => x.Location).Returns(loc); - - return diagnostic.Object; - } - - #endregion - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SuppressionExecutionContextTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SuppressionExecutionContextTests.cs deleted file mode 100644 index 9571aaa649..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SuppressionExecutionContextTests.cs +++ /dev/null @@ -1,112 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Immutable; -using System.Threading; -using FluentAssertions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Text; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests -{ - [TestClass] - public class SuppressionExecutionContextTests - { - [TestMethod] - [DataRow(@"C:\project\SonarLint for Visual Studio\Bindings\solutionName1\CSharp\SonarLint.xml", "solutionName1")] - [DataRow(@"C:\project\SonarLint for Visual Studio\Bindings\solutionName2\VB\SonarLint.xml", "solutionName2")] - [DataRow(@"C:\project\SonarLint for Visual Studio\Bindings\solutionName3\VB\sonarlint.xml", "solutionName3")] - [DataRow(@"C:\project\SonarLint for Visual Studio\Bindings\solutionName4\VB\SONARLINT.xml", "solutionName4")] - public void SonarProjectKey_PathIsValid_ReturnsExpectedSolutionName(string path, string solutionName) - { - var additionalText = new ConcreteAdditionalText(path); - - var testSubject = CreateTestSubject(additionalText); - - testSubject.SettingsKey.Should().Be(solutionName); - testSubject.IsInConnectedMode.Should().BeTrue(); - testSubject.Mode.Should().Be("Connected"); - } - - [TestMethod] - public void SonarProjectKey_PathIsNotValid_ReturnsNull() - { - var additionalText = new ConcreteAdditionalText(@"C:\project\projectKey1\CSharp\SonarLint.xml"); - - var testSubject = CreateTestSubject(additionalText); - - testSubject.SettingsKey.Should().BeNull(); - testSubject.IsInConnectedMode.Should().BeFalse(); - testSubject.Mode.Should().Be("Standalone"); - } - - [TestMethod] - public void SonarProjectKey_HasMixedPaths_ReturnsExpectedSolutionName() - { - var additionalText1 = new ConcreteAdditionalText(@"C:\project\projectKey1\CSharp\SonarLint.xml"); - var additionalText2 = new ConcreteAdditionalText(@"C:\project\SonarLint for Visual Studio\Bindings\expectedSlnName\CSharp\SonarLint.xml"); - - var testSubject = CreateTestSubject(additionalText1, additionalText2); - - testSubject.SettingsKey.Should().Be("expectedSlnName"); - testSubject.IsInConnectedMode.Should().BeTrue(); - testSubject.Mode.Should().Be("Connected"); - } - - [TestMethod] - public void SonarProjectKey_HasNoPaths_ReturnsNull() - { - var testSubject = CreateTestSubject(); - - testSubject.SettingsKey.Should().BeNull(); - testSubject.IsInConnectedMode.Should().BeFalse(); - testSubject.Mode.Should().Be("Standalone"); - } - - private SuppressionExecutionContext CreateTestSubject(params AdditionalText[] existingAdditionalFiles) - { - var analyzerOptions = CreateAnalyzerOptions(existingAdditionalFiles); - return new SuppressionExecutionContext(analyzerOptions); - } - - private AnalyzerOptions CreateAnalyzerOptions(params AdditionalText[] existingAdditionalFiles) - { - return new AnalyzerOptions(existingAdditionalFiles.ToImmutableArray()); - } - - } - - internal class ConcreteAdditionalText : AdditionalText - { - internal ConcreteAdditionalText(string path) - { - Path = path; - } - - public override string Path { get; } - - public override SourceText GetText(CancellationToken cancellationToken = default) - { - return SourceText.From(string.Empty); - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/TestHelper.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/TestHelper.cs deleted file mode 100644 index adced96222..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/TestHelper.cs +++ /dev/null @@ -1,72 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests -{ - internal static class TestHelper - { - public static SuppressedIssue CreateIssue( - string ruleId = "ruleId", - string path = "path", - int? line = 0, - string hash = "hash", - RoslynLanguage language = RoslynLanguage.CSharp, - string issueServerKey = "key") => - new SuppressedIssue - { - FilePath = path, - Hash = hash, - RoslynLanguage = language, - RoslynRuleId = ruleId, - RoslynIssueLine = line, - IssueServerKey = issueServerKey - }; - - public static SonarQubeIssue CreateSonarQubeIssue( - string ruleId = "any", - int? line = null, - string filePath = "filePath", - string hash = "hash", - bool isSuppressed = true, - string issueKey = null) - { - var sonarQubeIssue = new SonarQubeIssue( - issueKey ?? Guid.NewGuid().ToString(), - filePath, - hash, - "message", - "moduleKey", - ruleId, - false, // isResolved - SonarQubeIssueSeverity.Info, - DateTimeOffset.UtcNow, - DateTimeOffset.UtcNow, - line.HasValue ? new IssueTextRange(line.Value, line.Value, 1, 999) : null, - null - ); - - sonarQubeIssue.IsResolved = isSuppressed; - - return sonarQubeIssue; - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/Container.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/Container.cs deleted file mode 100644 index d840de4eb6..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/Container.cs +++ /dev/null @@ -1,83 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.IO; -using System.Threading; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Logging; -using SonarLint.VisualStudio.Roslyn.Suppressions.Settings.Cache; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions -{ - internal interface IContainer : IDisposable - { - ILogger Logger { get; } - - ISuppressionChecker SuppressionChecker { get; } - } - - internal sealed class Container : IContainer - { - /// - /// We do not provide ValueFactory in the initialization so that exceptions would not be cached. - /// https://docs.microsoft.com/en-us/dotnet/api/system.threading.lazythreadsafetymode?view=net-6.0 - /// - private static readonly Lazy _instance = new Lazy(LazyThreadSafetyMode.ExecutionAndPublication); - - public static IContainer Instance - { - get - { - try - { - return _instance.Value; - } - catch - { - return null; - } - } - } - - private readonly ISuppressedIssuesFileWatcher fileWatcher; - - public ILogger Logger { get; } - - public ISuppressionChecker SuppressionChecker { get; } - - public Container() - { - Directory.CreateDirectory(RoslynSettingsFileInfo.Directory); - Logger = LoggerFactory.Default.Create(new SystemDebugLoggerWriter(), new EnableAllLoggerSettingsProvider()); - - var settingsCache = new SettingsCache(Logger); - fileWatcher = new SuppressedIssuesFileWatcher(settingsCache, Logger); - - SuppressionChecker = new SuppressionChecker(settingsCache); - } - - public void Dispose() - { - fileWatcher?.Dispose(); - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/ISuppressedIssuesCalculator.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/ISuppressedIssuesCalculator.cs deleted file mode 100644 index aa79440005..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/ISuppressedIssuesCalculator.cs +++ /dev/null @@ -1,41 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.InProcess -{ - internal interface ISuppressedIssuesCalculator - { - /// - /// Returns the suppressed issues that should be added to the settings file, or null if no changes are needed. - /// - IEnumerable GetSuppressedIssuesOrNull(string roslynSettingsKey); - } - - internal interface ISuppressedIssuesCalculatorFactory - { - ISuppressedIssuesCalculator CreateAllSuppressedIssuesCalculator(IEnumerable sonarQubeIssues); - - ISuppressedIssuesCalculator CreateNewSuppressedIssuesCalculator(IEnumerable sonarQubeIssues); - - ISuppressedIssuesCalculator CreateSuppressedIssuesRemovedCalculator(IEnumerable issueServerKeys); - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/IssueConverter.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/IssueConverter.cs deleted file mode 100644 index a46e02d968..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/IssueConverter.cs +++ /dev/null @@ -1,76 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.InProcess; - -// Converts SonarQube issues to SuppressedIssues that can be compared more easily with Roslyn issues -internal static class IssueConverter -{ - public static SuppressedIssue Convert(SonarQubeIssue issue) - { - var (repoKey, ruleKey) = GetRepoAndRuleKey(issue.RuleId); - var language = GetRoslynLanguage(repoKey); - - var line = issue.TextRange == null ? (int?)null : issue.TextRange.StartLine - 1; - return new SuppressedIssue - { - RoslynRuleId = ruleKey, - FilePath = issue.FilePath, - Hash = issue.Hash, - RoslynLanguage = language, - RoslynIssueLine = line, - IssueServerKey = issue.IssueKey - }; - } - - private static (string repoKey, string ruleKey) GetRepoAndRuleKey(string sonarRuleId) - { - // Sonar rule ids are in the form "[repo key]:[rule key]" - var separatorPos = sonarRuleId.IndexOf(":", StringComparison.OrdinalIgnoreCase); - if (separatorPos > -1) - { - var repoKey = sonarRuleId.Substring(0, separatorPos); - var ruleKey = sonarRuleId.Substring(separatorPos + 1); - - return (repoKey, ruleKey); - } - - return (null, null); // invalid rule key -> ignore - } - - private static RoslynLanguage GetRoslynLanguage(string repoKey) - { - // Currently the only Sonar repos which contain Roslyn analysis rules are - // csharpsquid and vbnet. These include "normal" and "hotspot" rules. - // The taint rules are in a different repo, and the part that is implemented - // as a Roslyn analyzer won't raise issues anyway. - switch (repoKey) - { - case "csharpsquid": // i.e. the rules in SonarAnalyzer.CSharp - return RoslynLanguage.CSharp; - case "vbnet": // i.e. SonarAnalyzer.VisualBasic - return RoslynLanguage.VB; - default: - return RoslynLanguage.Unknown; - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/RoslynSettingsFileSynchronizer.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/RoslynSettingsFileSynchronizer.cs deleted file mode 100644 index df3e929ebc..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/RoslynSettingsFileSynchronizer.cs +++ /dev/null @@ -1,188 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using Microsoft.VisualStudio.Threading; -using SonarLint.VisualStudio.ConnectedMode.Persistence; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.Core.ETW; -using SonarLint.VisualStudio.Infrastructure.VS; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.InProcess; - -/// -/// Responsible for listening to server suppression events and calling -/// with the new suppressions. -/// -public interface IRoslynSettingsFileSynchronizer : IDisposable -{ -} - -[Export(typeof(IRoslynSettingsFileSynchronizer))] -[PartCreationPolicy(CreationPolicy.NonShared)] // stateless - doesn't need to be shared -internal sealed class RoslynSettingsFileSynchronizer : IRoslynSettingsFileSynchronizer -{ - private readonly IConfigurationProvider configurationProvider; - private readonly ISuppressedIssuesCalculatorFactory suppressedIssuesCalculatorFactory; - private readonly IRoslynSettingsFileStorage roslynSettingsFileStorage; - private readonly ISolutionInfoProvider solutionInfoProvider; - private readonly ISolutionBindingRepository solutionBindingRepository; - private readonly IRoslynSuppressionUpdater roslynSuppressionUpdater; - private readonly IThreadHandling threadHandling; - private readonly object lockObject = new(); - - [ImportingConstructor] - public RoslynSettingsFileSynchronizer( - IRoslynSettingsFileStorage roslynSettingsFileStorage, - IConfigurationProvider configurationProvider, - ISolutionInfoProvider solutionInfoProvider, - ISolutionBindingRepository solutionBindingRepository, - IRoslynSuppressionUpdater roslynSuppressionUpdater, - ISuppressedIssuesCalculatorFactory suppressedIssuesCalculatorFactory) - : this(roslynSettingsFileStorage, - configurationProvider, - solutionInfoProvider, - solutionBindingRepository, - roslynSuppressionUpdater, - suppressedIssuesCalculatorFactory, - ThreadHandling.Instance) - { - } - - internal RoslynSettingsFileSynchronizer( - IRoslynSettingsFileStorage roslynSettingsFileStorage, - IConfigurationProvider configurationProvider, - ISolutionInfoProvider solutionInfoProvider, - ISolutionBindingRepository solutionBindingRepository, - IRoslynSuppressionUpdater roslynSuppressionUpdater, - ISuppressedIssuesCalculatorFactory suppressedIssuesCalculatorFactory, - IThreadHandling threadHandling) - { - this.roslynSettingsFileStorage = roslynSettingsFileStorage; - this.configurationProvider = configurationProvider; - this.solutionInfoProvider = solutionInfoProvider; - this.solutionBindingRepository = solutionBindingRepository; - this.roslynSuppressionUpdater = roslynSuppressionUpdater; - this.suppressedIssuesCalculatorFactory = suppressedIssuesCalculatorFactory; - this.threadHandling = threadHandling; - - this.roslynSuppressionUpdater.SuppressedIssuesReloaded += OnSuppressedIssuesReloaded; - this.roslynSuppressionUpdater.NewIssuesSuppressed += OnNewIssuesSuppressed; - this.roslynSuppressionUpdater.SuppressionsRemoved += OnRoslynSuppressionsRemoved; - solutionBindingRepository.BindingDeleted += OnBindingDeleted; - } - - private void OnBindingDeleted(object sender, LocalBindingKeyEventArgs e) => roslynSettingsFileStorage.Delete(e.LocalBindingKey); - - public void Dispose() - { - solutionBindingRepository.BindingDeleted -= OnBindingDeleted; - roslynSuppressionUpdater.SuppressedIssuesReloaded -= OnSuppressedIssuesReloaded; - roslynSuppressionUpdater.NewIssuesSuppressed -= OnNewIssuesSuppressed; - roslynSuppressionUpdater.SuppressionsRemoved -= OnRoslynSuppressionsRemoved; - } - - private void OnSuppressedIssuesReloaded(object sender, SuppressionsEventArgs e) => - UpdateFileStorageAsync(suppressedIssuesCalculatorFactory.CreateAllSuppressedIssuesCalculator(e.SuppressedIssues)).Forget(); - - private void OnNewIssuesSuppressed(object sender, SuppressionsEventArgs e) - { - if (!e.SuppressedIssues.Any()) - { - return; - } - - UpdateFileStorageAsync(suppressedIssuesCalculatorFactory.CreateNewSuppressedIssuesCalculator(e.SuppressedIssues)).Forget(); - } - - private void OnRoslynSuppressionsRemoved(object sender, SuppressionsRemovedEventArgs e) - { - if (!e.IssueServerKeys.Any()) - { - return; - } - - UpdateFileStorageAsync(suppressedIssuesCalculatorFactory.CreateSuppressedIssuesRemovedCalculator(e.IssueServerKeys)).Forget(); - } - - /// - /// Updates the Roslyn suppressed issues file if in connected mode - /// - private async Task UpdateFileStorageAsync(ISuppressedIssuesCalculator suppressedIssuesCalculator) => - await threadHandling.RunOnBackgroundThread(async () => - { - await UpdateFileStorageIfNeededAsync(suppressedIssuesCalculator); - return true; - }); - - private async Task UpdateFileStorageIfNeededAsync(ISuppressedIssuesCalculator suppressedIssuesCalculator) - { - CodeMarkers.Instance.FileSynchronizerUpdateStart(); - try - { - var solutionNameWithoutExtension = await solutionInfoProvider.GetSolutionNameAsync(); - if (string.IsNullOrEmpty(solutionNameWithoutExtension)) - { - return; - } - - var sonarProjectKey = configurationProvider.GetConfiguration().Project?.ServerProjectKey; - if (string.IsNullOrEmpty(sonarProjectKey)) - { - SafeDeleteRoslynSettingsFileStorage(solutionNameWithoutExtension); - return; - } - - SafeUpdateRoslynSettingsFileStorage(suppressedIssuesCalculator, solutionNameWithoutExtension, sonarProjectKey); - } - finally - { - CodeMarkers.Instance.FileSynchronizerUpdateStop(); - } - } - - private void SafeDeleteRoslynSettingsFileStorage(string solutionNameWithoutExtension) - { - lock (lockObject) - { - roslynSettingsFileStorage.Delete(solutionNameWithoutExtension); - } - } - - private void SafeUpdateRoslynSettingsFileStorage( - ISuppressedIssuesCalculator suppressedIssuesCalculator, - string solutionNameWithoutExtension, - string sonarProjectKey) - { - lock (lockObject) - { - var suppressionsToAdd = suppressedIssuesCalculator.GetSuppressedIssuesOrNull(solutionNameWithoutExtension); - if (suppressionsToAdd == null) - { - return; - } - var roslynSettings = new RoslynSettings { SonarProjectKey = sonarProjectKey, Suppressions = suppressionsToAdd }; - roslynSettingsFileStorage.Update(roslynSettings, solutionNameWithoutExtension); - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/SuppressedIssuesCalculator.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/SuppressedIssuesCalculator.cs deleted file mode 100644 index deeffac062..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/SuppressedIssuesCalculator.cs +++ /dev/null @@ -1,85 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.InProcess; - -internal abstract class SuppressedIssuesCalculatorBase : ISuppressedIssuesCalculator -{ - public abstract IEnumerable GetSuppressedIssuesOrNull(string roslynSettingsKey); - - protected static SuppressedIssue[] GetRoslynSuppressedIssues(IEnumerable sonarQubeIssues) - { - var suppressionsToAdd = sonarQubeIssues - .Where(x => x.IsResolved) - .Select(IssueConverter.Convert) - .Where(x => x.RoslynLanguage != RoslynLanguage.Unknown && !string.IsNullOrEmpty(x.RoslynRuleId)) - .ToArray(); - return suppressionsToAdd; - } -} - -internal class AllSuppressedIssuesCalculator(ILogger logger, IEnumerable sonarQubeIssues) : SuppressedIssuesCalculatorBase -{ - public override IEnumerable GetSuppressedIssuesOrNull(string roslynSettingsKey) - { - logger.LogVerbose(Resources.Strings.RoslynSettingsFileSynchronizerReloadSuppressions); - return GetRoslynSuppressedIssues(sonarQubeIssues); - } -} - -internal class NewSuppressedIssuesCalculator(ILogger logger, IRoslynSettingsFileStorage roslynSettingsFileStorage, IEnumerable newSonarQubeIssues) : SuppressedIssuesCalculatorBase -{ - public override IEnumerable GetSuppressedIssuesOrNull(string roslynSettingsKey) - { - logger.LogVerbose(Resources.Strings.RoslynSettingsFileSynchronizerAddNewSuppressions); - - var suppressedIssuesInFile = roslynSettingsFileStorage.Get(roslynSettingsKey)?.Suppressions; - var suppressedIssuesToAdd = GetRoslynSuppressedIssues(newSonarQubeIssues); - if (suppressedIssuesInFile is null) - { - // if the settings do not exist on disk, add all the new suppressed issues - return suppressedIssuesToAdd; - } - - var suppressedIssuesToAddNotExistingInFile = suppressedIssuesToAdd.Where(newIssue => suppressedIssuesInFile.All(existing => !newIssue.AreSame(existing))); - return suppressedIssuesInFile.Concat(suppressedIssuesToAddNotExistingInFile); - } -} - -internal class SuppressedIssuesRemovedCalculator(ILogger logger, IRoslynSettingsFileStorage roslynSettingsFileStorage, IEnumerable resolvedIssueServerKeys) : SuppressedIssuesCalculatorBase -{ - public override IEnumerable GetSuppressedIssuesOrNull(string roslynSettingsKey) - { - var suppressedIssuesInFile = roslynSettingsFileStorage.Get(roslynSettingsKey)?.Suppressions?.ToList(); - var resolvedIssues = suppressedIssuesInFile?.Where(existingIssue => resolvedIssueServerKeys.Any(x => existingIssue.IssueServerKey == x)).ToList(); - if (resolvedIssues == null || !resolvedIssues.Any()) - { - // nothing to be done if no issue from file was resolved - return null; - } - - logger.LogVerbose(Resources.Strings.RoslynSettingsFileSynchronizerRemoveSuppressions); - return suppressedIssuesInFile.Except(resolvedIssues); - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/SuppressedIssuesCalculatorFactory.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/SuppressedIssuesCalculatorFactory.cs deleted file mode 100644 index 4176bed23f..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/SuppressedIssuesCalculatorFactory.cs +++ /dev/null @@ -1,43 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.InProcess; - -[Export(typeof(ISuppressedIssuesCalculatorFactory))] -[PartCreationPolicy(CreationPolicy.NonShared)] -[method: ImportingConstructor] -internal class SuppressedIssuesCalculatorFactory(ILogger logger, IRoslynSettingsFileStorage roslynSettingsFileStorage) : ISuppressedIssuesCalculatorFactory -{ - internal const string SuppressedIssuesCalculatorLogContext = "Suppressed Issues Calculator"; - private readonly ILogger logger = logger.ForContext(SuppressedIssuesCalculatorLogContext); - - public ISuppressedIssuesCalculator CreateAllSuppressedIssuesCalculator(IEnumerable sonarQubeIssues) => new AllSuppressedIssuesCalculator(logger, sonarQubeIssues); - - public ISuppressedIssuesCalculator CreateNewSuppressedIssuesCalculator(IEnumerable sonarQubeIssues) => - new NewSuppressedIssuesCalculator(logger, roslynSettingsFileStorage, sonarQubeIssues); - - public ISuppressedIssuesCalculator CreateSuppressedIssuesRemovedCalculator(IEnumerable issueServerKeys) => - new SuppressedIssuesRemovedCalculator(logger, roslynSettingsFileStorage, issueServerKeys); -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/InternalsVisibleTo.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/InternalsVisibleTo.cs deleted file mode 100644 index 270d3beb93..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/InternalsVisibleTo.cs +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Runtime.CompilerServices; - -#if SignAssembly -[assembly: InternalsVisibleTo("SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests,PublicKey=002400000480000094000000060200000024000052534131000400000100010081b4345a022cc0f4b42bdc795a5a7a1623c1e58dc2246645d751ad41ba98f2749dc5c4e0da3a9e09febcb2cd5b088a0f041f8ac24b20e736d8ae523061733782f9c4cd75b44f17a63714aced0b29a59cd1ce58d8e10ccdb6012c7098c39871043b7241ac4ab9f6b34f183db716082cd57c1ff648135bece256357ba735e67dc6")] -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] - -#else - -[assembly: InternalsVisibleTo("SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests")] -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] - -#endif diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/Resources/Strings.Designer.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/Resources/Strings.Designer.cs deleted file mode 100644 index 700274fa83..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/Resources/Strings.Designer.cs +++ /dev/null @@ -1,135 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.Resources { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Strings { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Strings() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SonarLint.VisualStudio.Roslyn.Suppressions.Resources.Strings", typeof(Strings).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to [Roslyn Suppressions] Error handling SonarQube for Visual Studio suppressions change. Issues suppressed on the server may not be suppressed in the IDE. Error: {0}. - /// - internal static string FileWatcherException { - get { - return ResourceManager.GetString("FileWatcherException", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to [Roslyn Suppressions] Error deleting the settings file for solution {0}. Error: {1}. - /// - internal static string RoslynSettingsFileStorageDeleteError { - get { - return ResourceManager.GetString("RoslynSettingsFileStorageDeleteError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Settings File was not found. - /// - internal static string RoslynSettingsFileStorageFileNotFound { - get { - return ResourceManager.GetString("RoslynSettingsFileStorageFileNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to [Roslyn Suppressions] Error loading settings for project {0}. Issues suppressed on the server will not be suppressed in the IDE. Error: {1}. - /// - internal static string RoslynSettingsFileStorageGetError { - get { - return ResourceManager.GetString("RoslynSettingsFileStorageGetError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to [Roslyn Suppressions] Error writing settings for project {0}. Issues suppressed on the server may not be suppressed in the IDE. Error: {1}. - /// - internal static string RoslynSettingsFileStorageUpdateError { - get { - return ResourceManager.GetString("RoslynSettingsFileStorageUpdateError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Adding new suppressions.... - /// - internal static string RoslynSettingsFileSynchronizerAddNewSuppressions { - get { - return ResourceManager.GetString("RoslynSettingsFileSynchronizerAddNewSuppressions", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Reloading issue suppressions.... - /// - internal static string RoslynSettingsFileSynchronizerReloadSuppressions { - get { - return ResourceManager.GetString("RoslynSettingsFileSynchronizerReloadSuppressions", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Removing suppressions.... - /// - internal static string RoslynSettingsFileSynchronizerRemoveSuppressions { - get { - return ResourceManager.GetString("RoslynSettingsFileSynchronizerRemoveSuppressions", resourceCulture); - } - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/Resources/Strings.resx b/src/Roslyn.Suppressions/Roslyn.Suppressions/Resources/Strings.resx deleted file mode 100644 index b8d2e133a8..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/Resources/Strings.resx +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - [Roslyn Suppressions] Error handling SonarQube for Visual Studio suppressions change. Issues suppressed on the server may not be suppressed in the IDE. Error: {0} - - - [Roslyn Suppressions] Error deleting the settings file for solution {0}. Error: {1} - - - Settings File was not found - - - [Roslyn Suppressions] Error loading settings for project {0}. Issues suppressed on the server will not be suppressed in the IDE. Error: {1} - - - [Roslyn Suppressions] Error writing settings for project {0}. Issues suppressed on the server may not be suppressed in the IDE. Error: {1} - - - Adding new suppressions... - - - Reloading issue suppressions... - - - Removing suppressions... - - \ No newline at end of file diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/Roslyn.Suppressions.csproj b/src/Roslyn.Suppressions/Roslyn.Suppressions/Roslyn.Suppressions.csproj deleted file mode 100644 index 5261268bf8..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/Roslyn.Suppressions.csproj +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - {082D5D8E-F914-4139-9AE3-3F48B679E3DA} - SonarLint.VisualStudio.Roslyn.Suppressions - SonarLint.VisualStudio.Roslyn.Suppressions - true - true - - - - - - - - - - - - - - True - SupportedSuppressionBuilder.tt - - - - - - True - True - Strings.resx - - - - - - ResXFileCodeGenerator - Strings.Designer.cs - - - - - - - - - - diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/RoslynSettings.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/RoslynSettings.cs deleted file mode 100644 index eed0b94c92..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/RoslynSettings.cs +++ /dev/null @@ -1,112 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions -{ - /// - /// Data class - contains all information that needs to be passed between the main VS - /// process and the external Roslyn analysis process - /// - internal class RoslynSettings - { - public static readonly RoslynSettings Empty = new RoslynSettings { Suppressions = Enumerable.Empty() }; - - /// - /// The actual Sonar project key to which the settings relate - /// - /// Note: this is not necessarily the same as the "settings key" - the settings key is a modified version of the - /// project key without any invalid file characters. - [JsonProperty("sonarProjectKey")] - public string SonarProjectKey { get; set; } - - [JsonProperty("suppressions")] - public IEnumerable Suppressions { get; set; } - } - - // Used as a parameter in data-driven tests, so needs to be public - public enum RoslynLanguage - { - Unknown = 0, - CSharp = 1, - VB = 2 - } - - /// - /// Describes a single C#/VB.NET issue that has been suppressed on the server - /// - /// This class contains the subset of fields from - /// needed to match suppressed issues against "live" Roslyn issues - internal class SuppressedIssue - { - /// - /// Relative file path - /// - /// - /// The path is relative to the Sonar project root. - /// The path is in Windows format i.e. the directory separators are backslashes - /// - [JsonProperty("file")] - public string FilePath { get; set; } - - [JsonProperty("hash")] - public string Hash { get; set; } - - [JsonProperty("lang")] - [JsonConverter(typeof(StringEnumConverter))] - public RoslynLanguage RoslynLanguage { get; set; } - - /// - /// The rule ID reported by the Roslyn analyzer e.g. S123 - /// - /// This Sonar rule key without repository key. - [JsonProperty("rule")] - public string RoslynRuleId { get; set; } - - /// - /// The 0-based line for the issue. Will be null for file-level issues - /// - /// Roslyn issues are 0-based - Sonar issues are 1-based. - [JsonProperty("line")] - public int? RoslynIssueLine { get; set; } - - /// - /// The key of the issue from the server - /// - [JsonProperty("issueServerKey")] - public string IssueServerKey { get; set; } - - public bool AreSame(SuppressedIssue other) - { - if (other == null) - { - return false; - } - return FilePath == other.FilePath && - Hash == other.Hash && - RoslynLanguage == other.RoslynLanguage && - RoslynRuleId == other.RoslynRuleId && - RoslynIssueLine == other.RoslynIssueLine && - IssueServerKey == other.IssueServerKey; - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/Settings.Cache/ISettingsCache.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/Settings.Cache/ISettingsCache.cs deleted file mode 100644 index 38edfd8313..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/Settings.Cache/ISettingsCache.cs +++ /dev/null @@ -1,33 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.Settings.Cache -{ - internal interface ISettingsCache - { - /// - /// Returns the settings for the specified key, or if - /// there are no settings for that key - /// - RoslynSettings GetSettings(string settingsKey); - - void Invalidate(string settingsKey); - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/Settings.Cache/SettingsCache.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/Settings.Cache/SettingsCache.cs deleted file mode 100644 index ec6cf4b161..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/Settings.Cache/SettingsCache.cs +++ /dev/null @@ -1,60 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Concurrent; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; -using static SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile.RoslynSettingsFileInfo; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.Settings.Cache -{ - internal class SettingsCache : ISettingsCache - { - private readonly IRoslynSettingsFileStorage fileStorage; - private readonly ConcurrentDictionary settingsCollection; - - - public SettingsCache(ILogger logger) : this(new RoslynSettingsFileStorage(logger), new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase)) - { - } - - internal SettingsCache(IRoslynSettingsFileStorage fileStorage, ConcurrentDictionary settingsCollection) - { - this.fileStorage = fileStorage; - this.settingsCollection = settingsCollection; - } - - public RoslynSettings GetSettings(string settingsKey) - { - if (!settingsCollection.ContainsKey(settingsKey)) - { - var settings = fileStorage.Get(settingsKey) ?? RoslynSettings.Empty; - settingsCollection.AddOrUpdate(settingsKey, settings, (x,y) => settings); - } - return settingsCollection[settingsKey]; - } - - public void Invalidate(string settingsKey) - { - settingsCollection.TryRemove(settingsKey, out _); - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/SettingsFile/RoslynSettingsFileInfo.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/SettingsFile/RoslynSettingsFileInfo.cs deleted file mode 100644 index 3a2c63b52a..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/SettingsFile/RoslynSettingsFileInfo.cs +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.IO; -using SonarLint.VisualStudio.Core.Helpers; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile -{ - internal static class RoslynSettingsFileInfo - { - public static readonly string Directory = PathHelper.GetTempDirForTask(false, "Roslyn"); - - /// - /// Returns the full file path for the given solution name - /// - /// The solution name without the file extension e.g. "MySolution" - /// File will be in a shared location and would be able to be accessed by multiple instances of VS simulateneously - public static string GetSettingsFilePath(string solutionName) - { - var escapedName = PathHelper.EscapeFileName(NormalizeKey(solutionName)); - - var fileName = escapedName + ".json"; - - return Path.Combine(Directory, fileName); - } - - /// - /// Returns an identifier for the project settings to which the settings file relates. - /// - /// The identifier is *not* the actual project key since we can't recover it accurately from the file name - public static string GetSettingsKey(string filePath) - { - return Path.GetFileNameWithoutExtension(filePath); - } - - private static string NormalizeKey(string key) => key.ToLowerInvariant(); - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/SettingsFile/RoslynSettingsFileStorage.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/SettingsFile/RoslynSettingsFileStorage.cs deleted file mode 100644 index 63e457b5be..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/SettingsFile/RoslynSettingsFileStorage.cs +++ /dev/null @@ -1,136 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using System.IO.Abstractions; -using Newtonsoft.Json; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.ETW; -using SonarLint.VisualStudio.Roslyn.Suppressions.Resources; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; - -internal interface IRoslynSettingsFileStorage -{ - /// - /// Updates the Roslyn settings file on disc for the specified solution - /// - void Update(RoslynSettings settings, string solutionNameWithoutExtension); - - /// - /// Return the settings for the specific settings key, or null - /// if there are no settings for that key - /// - RoslynSettings Get(string settingsKey); - - /// - /// Deletes the Roslyn settings file on disc for the specified solution - /// - void Delete(string solutionNameWithoutExtension); -} - -[Export(typeof(IRoslynSettingsFileStorage))] -internal class RoslynSettingsFileStorage : IRoslynSettingsFileStorage -{ - private readonly IFileSystem fileSystem; - private readonly ILogger logger; - - [ImportingConstructor] - public RoslynSettingsFileStorage(ILogger logger) : this(logger, new FileSystem()) - { - } - - internal RoslynSettingsFileStorage(ILogger logger, IFileSystem fileSystem) - { - this.fileSystem = fileSystem; - this.logger = logger; - fileSystem.Directory.CreateDirectory(RoslynSettingsFileInfo.Directory); - } - - public RoslynSettings Get(string settingsKey) - { - Debug.Assert(settingsKey != null, "Not expecting settings to be null"); - - try - { - CodeMarkers.Instance.FileStorageGetStart(); - var filePath = RoslynSettingsFileInfo.GetSettingsFilePath(settingsKey); - - if (!fileSystem.File.Exists(filePath)) - { - logger.WriteLine(string.Format(Strings.RoslynSettingsFileStorageGetError, settingsKey, Strings.RoslynSettingsFileStorageFileNotFound)); - return null; - } - - var fileContent = fileSystem.File.ReadAllText(filePath); - return JsonConvert.DeserializeObject(fileContent); - } - catch (Exception ex) - { - logger.WriteLine(string.Format(Strings.RoslynSettingsFileStorageGetError, settingsKey, ex.Message)); - } - finally - { - CodeMarkers.Instance.FileStorageGetStop(); - } - return null; - } - - public void Delete(string solutionNameWithoutExtension) - { - try - { - CodeMarkers.Instance.FileStorageUpdateStart(); - var filePath = RoslynSettingsFileInfo.GetSettingsFilePath(solutionNameWithoutExtension); - fileSystem.File.Delete(filePath); - } - catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) - { - logger.LogVerbose(Strings.RoslynSettingsFileStorageDeleteError, solutionNameWithoutExtension, ex.Message); - } - finally - { - CodeMarkers.Instance.FileStorageUpdateStop(); - } - } - - public void Update(RoslynSettings settings, string solutionNameWithoutExtension) - { - Debug.Assert(settings != null, "Not expecting settings to be null"); - Debug.Assert(!string.IsNullOrWhiteSpace(settings.SonarProjectKey), "Not expecting settings.SonarProjectKey to be null"); - Debug.Assert(solutionNameWithoutExtension != null, "Not expecting solutionNameWithoutExtension to be null"); - - try - { - CodeMarkers.Instance.FileStorageUpdateStart(); - var filePath = RoslynSettingsFileInfo.GetSettingsFilePath(solutionNameWithoutExtension); - var fileContent = JsonConvert.SerializeObject(settings, Formatting.Indented); - fileSystem.File.WriteAllText(filePath, fileContent); - } - catch (Exception ex) - { - logger.WriteLine(string.Format(Strings.RoslynSettingsFileStorageUpdateError, settings.SonarProjectKey, ex.Message)); - } - finally - { - CodeMarkers.Instance.FileStorageUpdateStop(); - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/SettingsFile/RoslynSettingsFileWatcher.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/SettingsFile/RoslynSettingsFileWatcher.cs deleted file mode 100644 index 66f3abe902..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/SettingsFile/RoslynSettingsFileWatcher.cs +++ /dev/null @@ -1,109 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.IO; -using System.IO.Abstractions; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.ETW; -using SonarLint.VisualStudio.Roslyn.Suppressions.Resources; -using SonarLint.VisualStudio.Roslyn.Suppressions.Settings.Cache; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile -{ - /// - /// Monitors files created under directory and - /// calls when a sonarProject's settings file is changed. - /// - internal interface ISuppressedIssuesFileWatcher : IDisposable - { - } - - internal sealed class SuppressedIssuesFileWatcher : ISuppressedIssuesFileWatcher - { - private IFileSystemWatcher fileSystemWatcher; - private readonly ISettingsCache settingsCache; - private readonly ILogger logger; - - public SuppressedIssuesFileWatcher(ISettingsCache settingsCache, ILogger logger) - : this(settingsCache, logger, new FileSystem()) - { - } - - internal SuppressedIssuesFileWatcher(ISettingsCache settingsCache, ILogger logger, IFileSystem fileSystem) - { - this.settingsCache = settingsCache; - this.logger = logger; - - WatchSuppressionsDirectory(fileSystem); - } - - /// - /// Monitors files created/edited/deleted in folder. - /// - /// - /// This method can throw (for example if the directory does not exist yet). - /// It's a deliberate design choice to not catch exceptions and let the caller handle it. - /// This is because parts of the system need to be singletons and the caller should be the one responsible for handling that. - /// - private void WatchSuppressionsDirectory(IFileSystem fileSystem) - { - fileSystemWatcher = fileSystem.FileSystemWatcher.FromPath(RoslynSettingsFileInfo.Directory); - - fileSystemWatcher.Filter = "*.json"; - fileSystemWatcher.Created += InvalidateCache; - fileSystemWatcher.Changed += InvalidateCache; - fileSystemWatcher.Deleted += InvalidateCache; - - fileSystemWatcher.EnableRaisingEvents = true; - } - - private void InvalidateCache(object sender, FileSystemEventArgs e) - { - try - { - CodeMarkers.Instance.FileWatcherInvalidateStart(e.ChangeType.ToString()); - var fileName = e.Name; - var settingsKey = RoslynSettingsFileInfo.GetSettingsKey(fileName); - - if (!string.IsNullOrEmpty(fileName)) - { - settingsCache.Invalidate(settingsKey); - } - } - catch (Exception ex) - { - logger.WriteLine(Strings.FileWatcherException, ex); - } - finally - { - CodeMarkers.Instance.FileWatcherInvalidateStop(e.ChangeType.ToString()); - } - } - - public void Dispose() - { - fileSystemWatcher.Created -= InvalidateCache; - fileSystemWatcher.Changed -= InvalidateCache; - fileSystemWatcher.Deleted -= InvalidateCache; - fileSystemWatcher.Dispose(); - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/SonarDiagnosticSuppressor.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/SonarDiagnosticSuppressor.cs deleted file mode 100644 index 0e544de763..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/SonarDiagnosticSuppressor.cs +++ /dev/null @@ -1,88 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using SonarLint.VisualStudio.Core.ETW; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions -{ - /// - /// Diagnostic suppressor that can suppress all Sonar C# and VB.NET diagnostics - /// - [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] - internal class SonarDiagnosticSuppressor : DiagnosticSuppressor - { - /// - /// Func is used so that we could initialize the container lazily only when we are in connected mode. - /// - private readonly Func getContainer; - - public SonarDiagnosticSuppressor () : this(() => Container.Instance) - { - - } - - internal SonarDiagnosticSuppressor(Func getContainer) - { - this.getContainer = getContainer; - } - - public override ImmutableArray SupportedSuppressions => SupportedSuppressionsBuilder.Instance.Descriptors; - - public override void ReportSuppressions(SuppressionAnalysisContext context) - { - CodeMarkers.Instance.ReportSuppressionsStart(); - var executionContext = new SuppressionExecutionContext(context.Options); - - var suppressions = GetSuppressions(context.ReportedDiagnostics, executionContext); - - //SuppressionAnalysisContext is a public struct with an internal constructor and because of that we can't mock or create it - //To be able the test we had to seperate parts of code that do not use SuppressionAnalysisContext directly and had to loop twice - foreach (var suppression in suppressions) - { - context.ReportSuppression(suppression); - } - CodeMarkers.Instance.ReportSuppressionsStop(executionContext.Mode, suppressions.Count()); - } - - internal /*For Testing*/ IEnumerable GetSuppressions(ImmutableArray ReportedDiagnostics, ISuppressionExecutionContext executionContext) - { - if (!executionContext.IsInConnectedMode) - { - return Enumerable.Empty(); - } - var result = new List(); - - var container = getContainer(); - - foreach (var diag in ReportedDiagnostics.Where(diag => container.SuppressionChecker.IsSuppressed(diag, executionContext.SettingsKey))) - { - var suppressionDesc = SupportedSuppressions.Single(x => x.SuppressedDiagnosticId == diag.Id); - result.Add(Suppression.Create(suppressionDesc, diag)); - } - return result; - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/SupportedSuppressionBuilder.g.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/SupportedSuppressionBuilder.g.cs deleted file mode 100644 index 0357141106..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/SupportedSuppressionBuilder.g.cs +++ /dev/null @@ -1,563 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using Microsoft.CodeAnalysis; -using System; -using System.Collections.Immutable; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions -{ - /// - /// Generated class that returns SupportedSuppressions for all Sonar C# and VB.NET rules - /// - internal sealed class SupportedSuppressionsBuilder - { - private static readonly Lazy lazy = new Lazy(() => new SupportedSuppressionsBuilder()); - - public static SupportedSuppressionsBuilder Instance => lazy.Value; - - public ImmutableArray Descriptors { get; } - - private SupportedSuppressionsBuilder() - { - Descriptors = GetDescriptors(); - } - - private static ImmutableArray GetDescriptors() - { - var descriptors = new SuppressionDescriptor[] - { - // ************************************************************************************************* - // If the number of diagnostic ids changes significantly or decreases when the analysers are updated, - // investigate! There may be a problem with code generator. - // ************************************************************************************************* - // Number of unique diagnostic ids (C# and VB.NET): 504 - CreateDescriptor("S100"), - CreateDescriptor("S1006"), - CreateDescriptor("S101"), - CreateDescriptor("S103"), - CreateDescriptor("S104"), - CreateDescriptor("S1048"), - CreateDescriptor("S105"), - CreateDescriptor("S106"), - CreateDescriptor("S1066"), - CreateDescriptor("S1067"), - CreateDescriptor("S107"), - CreateDescriptor("S1075"), - CreateDescriptor("S108"), - CreateDescriptor("S109"), - CreateDescriptor("S110"), - CreateDescriptor("S1104"), - CreateDescriptor("S1109"), - CreateDescriptor("S1110"), - CreateDescriptor("S1116"), - CreateDescriptor("S1117"), - CreateDescriptor("S1118"), - CreateDescriptor("S112"), - CreateDescriptor("S1121"), - CreateDescriptor("S1123"), - CreateDescriptor("S1125"), - CreateDescriptor("S1128"), - CreateDescriptor("S113"), - CreateDescriptor("S1133"), - CreateDescriptor("S1134"), - CreateDescriptor("S1135"), - CreateDescriptor("S114"), - CreateDescriptor("S1144"), - CreateDescriptor("S1147"), - CreateDescriptor("S1151"), - CreateDescriptor("S1155"), - CreateDescriptor("S1163"), - CreateDescriptor("S1168"), - CreateDescriptor("S117"), - CreateDescriptor("S1172"), - CreateDescriptor("S1185"), - CreateDescriptor("S1186"), - CreateDescriptor("S119"), - CreateDescriptor("S1192"), - CreateDescriptor("S1197"), - CreateDescriptor("S1199"), - CreateDescriptor("S1200"), - CreateDescriptor("S1206"), - CreateDescriptor("S121"), - CreateDescriptor("S1210"), - CreateDescriptor("S1215"), - CreateDescriptor("S122"), - CreateDescriptor("S1226"), - CreateDescriptor("S1227"), - CreateDescriptor("S1244"), - CreateDescriptor("S125"), - CreateDescriptor("S126"), - CreateDescriptor("S1264"), - CreateDescriptor("S127"), - CreateDescriptor("S1301"), - CreateDescriptor("S1309"), - CreateDescriptor("S131"), - CreateDescriptor("S1312"), - CreateDescriptor("S1313"), - CreateDescriptor("S134"), - CreateDescriptor("S138"), - CreateDescriptor("S139"), - CreateDescriptor("S1449"), - CreateDescriptor("S1450"), - CreateDescriptor("S1451"), - CreateDescriptor("S1479"), - CreateDescriptor("S1481"), - CreateDescriptor("S1541"), - CreateDescriptor("S1542"), - CreateDescriptor("S1607"), - CreateDescriptor("S1643"), - CreateDescriptor("S1645"), - CreateDescriptor("S1654"), - CreateDescriptor("S1656"), - CreateDescriptor("S1659"), - CreateDescriptor("S1694"), - CreateDescriptor("S1696"), - CreateDescriptor("S1698"), - CreateDescriptor("S1699"), - CreateDescriptor("S1751"), - CreateDescriptor("S1764"), - CreateDescriptor("S1821"), - CreateDescriptor("S1848"), - CreateDescriptor("S1854"), - CreateDescriptor("S1858"), - CreateDescriptor("S1862"), - CreateDescriptor("S1871"), - CreateDescriptor("S1905"), - CreateDescriptor("S1939"), - CreateDescriptor("S1940"), - CreateDescriptor("S1944"), - CreateDescriptor("S1994"), - CreateDescriptor("S2053"), - CreateDescriptor("S2068"), - CreateDescriptor("S2077"), - CreateDescriptor("S2092"), - CreateDescriptor("S2094"), - CreateDescriptor("S2114"), - CreateDescriptor("S2115"), - CreateDescriptor("S2123"), - CreateDescriptor("S2139"), - CreateDescriptor("S2148"), - CreateDescriptor("S2156"), - CreateDescriptor("S2166"), - CreateDescriptor("S2178"), - CreateDescriptor("S2183"), - CreateDescriptor("S2184"), - CreateDescriptor("S2187"), - CreateDescriptor("S2190"), - CreateDescriptor("S2197"), - CreateDescriptor("S2198"), - CreateDescriptor("S2201"), - CreateDescriptor("S2219"), - CreateDescriptor("S2221"), - CreateDescriptor("S2222"), - CreateDescriptor("S2223"), - CreateDescriptor("S2225"), - CreateDescriptor("S2234"), - CreateDescriptor("S2245"), - CreateDescriptor("S2251"), - CreateDescriptor("S2252"), - CreateDescriptor("S2257"), - CreateDescriptor("S2259"), - CreateDescriptor("S2275"), - CreateDescriptor("S2290"), - CreateDescriptor("S2291"), - CreateDescriptor("S2292"), - CreateDescriptor("S2302"), - CreateDescriptor("S2304"), - CreateDescriptor("S2306"), - CreateDescriptor("S2325"), - CreateDescriptor("S2326"), - CreateDescriptor("S2327"), - CreateDescriptor("S2328"), - CreateDescriptor("S2330"), - CreateDescriptor("S2333"), - CreateDescriptor("S2339"), - CreateDescriptor("S2340"), - CreateDescriptor("S2342"), - CreateDescriptor("S2343"), - CreateDescriptor("S2344"), - CreateDescriptor("S2345"), - CreateDescriptor("S2346"), - CreateDescriptor("S2347"), - CreateDescriptor("S2348"), - CreateDescriptor("S2349"), - CreateDescriptor("S2352"), - CreateDescriptor("S2354"), - CreateDescriptor("S2355"), - CreateDescriptor("S2357"), - CreateDescriptor("S2358"), - CreateDescriptor("S2359"), - CreateDescriptor("S2360"), - CreateDescriptor("S2362"), - CreateDescriptor("S2363"), - CreateDescriptor("S2364"), - CreateDescriptor("S2365"), - CreateDescriptor("S2366"), - CreateDescriptor("S2367"), - CreateDescriptor("S2368"), - CreateDescriptor("S2369"), - CreateDescriptor("S2370"), - CreateDescriptor("S2372"), - CreateDescriptor("S2373"), - CreateDescriptor("S2374"), - CreateDescriptor("S2375"), - CreateDescriptor("S2376"), - CreateDescriptor("S2386"), - CreateDescriptor("S2387"), - CreateDescriptor("S2429"), - CreateDescriptor("S2436"), - CreateDescriptor("S2437"), - CreateDescriptor("S2445"), - CreateDescriptor("S2479"), - CreateDescriptor("S2486"), - CreateDescriptor("S2551"), - CreateDescriptor("S2583"), - CreateDescriptor("S2589"), - CreateDescriptor("S2612"), - CreateDescriptor("S2629"), - CreateDescriptor("S2674"), - CreateDescriptor("S2681"), - CreateDescriptor("S2688"), - CreateDescriptor("S2692"), - CreateDescriptor("S2696"), - CreateDescriptor("S2699"), - CreateDescriptor("S2701"), - CreateDescriptor("S2737"), - CreateDescriptor("S2743"), - CreateDescriptor("S2755"), - CreateDescriptor("S2757"), - CreateDescriptor("S2760"), - CreateDescriptor("S2761"), - CreateDescriptor("S2857"), - CreateDescriptor("S2925"), - CreateDescriptor("S2930"), - CreateDescriptor("S2931"), - CreateDescriptor("S2933"), - CreateDescriptor("S2934"), - CreateDescriptor("S2951"), - CreateDescriptor("S2952"), - CreateDescriptor("S2953"), - CreateDescriptor("S2955"), - CreateDescriptor("S2970"), - CreateDescriptor("S2971"), - CreateDescriptor("S2995"), - CreateDescriptor("S2996"), - CreateDescriptor("S2997"), - CreateDescriptor("S3005"), - CreateDescriptor("S3010"), - CreateDescriptor("S3011"), - CreateDescriptor("S3052"), - CreateDescriptor("S3059"), - CreateDescriptor("S3060"), - CreateDescriptor("S3063"), - CreateDescriptor("S3168"), - CreateDescriptor("S3169"), - CreateDescriptor("S3172"), - CreateDescriptor("S3215"), - CreateDescriptor("S3216"), - CreateDescriptor("S3217"), - CreateDescriptor("S3218"), - CreateDescriptor("S3220"), - CreateDescriptor("S3234"), - CreateDescriptor("S3235"), - CreateDescriptor("S3236"), - CreateDescriptor("S3237"), - CreateDescriptor("S3240"), - CreateDescriptor("S3241"), - CreateDescriptor("S3242"), - CreateDescriptor("S3244"), - CreateDescriptor("S3246"), - CreateDescriptor("S3247"), - CreateDescriptor("S3249"), - CreateDescriptor("S3251"), - CreateDescriptor("S3253"), - CreateDescriptor("S3254"), - CreateDescriptor("S3256"), - CreateDescriptor("S3257"), - CreateDescriptor("S3260"), - CreateDescriptor("S3261"), - CreateDescriptor("S3262"), - CreateDescriptor("S3263"), - CreateDescriptor("S3264"), - CreateDescriptor("S3265"), - CreateDescriptor("S3267"), - CreateDescriptor("S3329"), - CreateDescriptor("S3330"), - CreateDescriptor("S3343"), - CreateDescriptor("S3346"), - CreateDescriptor("S3353"), - CreateDescriptor("S3358"), - CreateDescriptor("S3363"), - CreateDescriptor("S3366"), - CreateDescriptor("S3376"), - CreateDescriptor("S3385"), - CreateDescriptor("S3397"), - CreateDescriptor("S3398"), - CreateDescriptor("S3400"), - CreateDescriptor("S3415"), - CreateDescriptor("S3416"), - CreateDescriptor("S3427"), - CreateDescriptor("S3431"), - CreateDescriptor("S3433"), - CreateDescriptor("S3440"), - CreateDescriptor("S3441"), - CreateDescriptor("S3442"), - CreateDescriptor("S3443"), - CreateDescriptor("S3444"), - CreateDescriptor("S3445"), - CreateDescriptor("S3447"), - CreateDescriptor("S3449"), - CreateDescriptor("S3450"), - CreateDescriptor("S3451"), - CreateDescriptor("S3453"), - CreateDescriptor("S3456"), - CreateDescriptor("S3457"), - CreateDescriptor("S3458"), - CreateDescriptor("S3459"), - CreateDescriptor("S3464"), - CreateDescriptor("S3466"), - CreateDescriptor("S3532"), - CreateDescriptor("S3597"), - CreateDescriptor("S3598"), - CreateDescriptor("S3600"), - CreateDescriptor("S3603"), - CreateDescriptor("S3604"), - CreateDescriptor("S3610"), - CreateDescriptor("S3626"), - CreateDescriptor("S3655"), - CreateDescriptor("S3717"), - CreateDescriptor("S3776"), - CreateDescriptor("S3860"), - CreateDescriptor("S3866"), - CreateDescriptor("S3869"), - CreateDescriptor("S3871"), - CreateDescriptor("S3872"), - CreateDescriptor("S3874"), - CreateDescriptor("S3875"), - CreateDescriptor("S3876"), - CreateDescriptor("S3877"), - CreateDescriptor("S3878"), - CreateDescriptor("S3880"), - CreateDescriptor("S3881"), - CreateDescriptor("S3884"), - CreateDescriptor("S3885"), - CreateDescriptor("S3887"), - CreateDescriptor("S3889"), - CreateDescriptor("S3897"), - CreateDescriptor("S3898"), - CreateDescriptor("S3900"), - CreateDescriptor("S3902"), - CreateDescriptor("S3903"), - CreateDescriptor("S3904"), - CreateDescriptor("S3906"), - CreateDescriptor("S3908"), - CreateDescriptor("S3909"), - CreateDescriptor("S3923"), - CreateDescriptor("S3925"), - CreateDescriptor("S3926"), - CreateDescriptor("S3927"), - CreateDescriptor("S3928"), - CreateDescriptor("S3937"), - CreateDescriptor("S3949"), - CreateDescriptor("S3956"), - CreateDescriptor("S3962"), - CreateDescriptor("S3963"), - CreateDescriptor("S3966"), - CreateDescriptor("S3967"), - CreateDescriptor("S3971"), - CreateDescriptor("S3972"), - CreateDescriptor("S3973"), - CreateDescriptor("S3981"), - CreateDescriptor("S3984"), - CreateDescriptor("S3990"), - CreateDescriptor("S3992"), - CreateDescriptor("S3993"), - CreateDescriptor("S3994"), - CreateDescriptor("S3995"), - CreateDescriptor("S3996"), - CreateDescriptor("S3997"), - CreateDescriptor("S3998"), - CreateDescriptor("S4000"), - CreateDescriptor("S4002"), - CreateDescriptor("S4004"), - CreateDescriptor("S4005"), - CreateDescriptor("S4015"), - CreateDescriptor("S4016"), - CreateDescriptor("S4017"), - CreateDescriptor("S4018"), - CreateDescriptor("S4019"), - CreateDescriptor("S4022"), - CreateDescriptor("S4023"), - CreateDescriptor("S4025"), - CreateDescriptor("S4026"), - CreateDescriptor("S4027"), - CreateDescriptor("S4035"), - CreateDescriptor("S4036"), - CreateDescriptor("S4039"), - CreateDescriptor("S4040"), - CreateDescriptor("S4041"), - CreateDescriptor("S4047"), - CreateDescriptor("S4049"), - CreateDescriptor("S4050"), - CreateDescriptor("S4052"), - CreateDescriptor("S4055"), - CreateDescriptor("S4056"), - CreateDescriptor("S4057"), - CreateDescriptor("S4058"), - CreateDescriptor("S4059"), - CreateDescriptor("S4060"), - CreateDescriptor("S4061"), - CreateDescriptor("S4069"), - CreateDescriptor("S4070"), - CreateDescriptor("S4136"), - CreateDescriptor("S4143"), - CreateDescriptor("S4144"), - CreateDescriptor("S4158"), - CreateDescriptor("S4159"), - CreateDescriptor("S4200"), - CreateDescriptor("S4201"), - CreateDescriptor("S4210"), - CreateDescriptor("S4211"), - CreateDescriptor("S4212"), - CreateDescriptor("S4214"), - CreateDescriptor("S4220"), - CreateDescriptor("S4225"), - CreateDescriptor("S4226"), - CreateDescriptor("S4260"), - CreateDescriptor("S4261"), - CreateDescriptor("S4275"), - CreateDescriptor("S4277"), - CreateDescriptor("S4347"), - CreateDescriptor("S4423"), - CreateDescriptor("S4426"), - CreateDescriptor("S4428"), - CreateDescriptor("S4433"), - CreateDescriptor("S4456"), - CreateDescriptor("S4457"), - CreateDescriptor("S4462"), - CreateDescriptor("S4487"), - CreateDescriptor("S4502"), - CreateDescriptor("S4507"), - CreateDescriptor("S4524"), - CreateDescriptor("S4545"), - CreateDescriptor("S4581"), - CreateDescriptor("S4583"), - CreateDescriptor("S4586"), - CreateDescriptor("S4635"), - CreateDescriptor("S4663"), - CreateDescriptor("S4790"), - CreateDescriptor("S4792"), - CreateDescriptor("S4830"), - CreateDescriptor("S5034"), - CreateDescriptor("S5042"), - CreateDescriptor("S5122"), - CreateDescriptor("S5332"), - CreateDescriptor("S5344"), - CreateDescriptor("S5443"), - CreateDescriptor("S5445"), - CreateDescriptor("S5542"), - CreateDescriptor("S5547"), - CreateDescriptor("S5659"), - CreateDescriptor("S5693"), - CreateDescriptor("S5753"), - CreateDescriptor("S5766"), - CreateDescriptor("S5773"), - CreateDescriptor("S5856"), - CreateDescriptor("S5944"), - CreateDescriptor("S6145"), - CreateDescriptor("S6146"), - CreateDescriptor("S6354"), - CreateDescriptor("S6377"), - CreateDescriptor("S6418"), - CreateDescriptor("S6419"), - CreateDescriptor("S6420"), - CreateDescriptor("S6421"), - CreateDescriptor("S6422"), - CreateDescriptor("S6423"), - CreateDescriptor("S6424"), - CreateDescriptor("S6444"), - CreateDescriptor("S6507"), - CreateDescriptor("S6513"), - CreateDescriptor("S6561"), - CreateDescriptor("S6562"), - CreateDescriptor("S6563"), - CreateDescriptor("S6566"), - CreateDescriptor("S6575"), - CreateDescriptor("S6580"), - CreateDescriptor("S6585"), - CreateDescriptor("S6588"), - CreateDescriptor("S6602"), - CreateDescriptor("S6603"), - CreateDescriptor("S6605"), - CreateDescriptor("S6607"), - CreateDescriptor("S6608"), - CreateDescriptor("S6609"), - CreateDescriptor("S6610"), - CreateDescriptor("S6612"), - CreateDescriptor("S6613"), - CreateDescriptor("S6617"), - CreateDescriptor("S6618"), - CreateDescriptor("S6640"), - CreateDescriptor("S6664"), - CreateDescriptor("S6667"), - CreateDescriptor("S6668"), - CreateDescriptor("S6669"), - CreateDescriptor("S6670"), - CreateDescriptor("S6672"), - CreateDescriptor("S6673"), - CreateDescriptor("S6674"), - CreateDescriptor("S6675"), - CreateDescriptor("S6677"), - CreateDescriptor("S6678"), - CreateDescriptor("S6781"), - CreateDescriptor("S6797"), - CreateDescriptor("S6798"), - CreateDescriptor("S6800"), - CreateDescriptor("S6802"), - CreateDescriptor("S6803"), - CreateDescriptor("S6930"), - CreateDescriptor("S6931"), - CreateDescriptor("S6932"), - CreateDescriptor("S6934"), - CreateDescriptor("S6960"), - CreateDescriptor("S6961"), - CreateDescriptor("S6962"), - CreateDescriptor("S6964"), - CreateDescriptor("S6965"), - CreateDescriptor("S6966"), - CreateDescriptor("S6967"), - CreateDescriptor("S6968"), - CreateDescriptor("S7039"), - CreateDescriptor("S7130"), - CreateDescriptor("S7131"), - CreateDescriptor("S7133"), - CreateDescriptor("S818"), - CreateDescriptor("S881"), - CreateDescriptor("S907"), - CreateDescriptor("S927"), - }; - return ImmutableArray.ToImmutableArray(descriptors); - } - - private static SuppressionDescriptor CreateDescriptor(string diagId) => - new SuppressionDescriptor("X" + diagId, diagId, "Suppressed on the Sonar server"); - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/SupportedSuppressionBuilder.tt b/src/Roslyn.Suppressions/Roslyn.Suppressions/SupportedSuppressionBuilder.tt deleted file mode 100644 index fac98c2b0d..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/SupportedSuppressionBuilder.tt +++ /dev/null @@ -1,194 +0,0 @@ -<#@ template debug="false" hostspecific="false" language="C#" #> -<#@ output extension=".g.cs" #> -<#@ assembly name="System.Core" #> -<#@ assembly name="$(SonarCSharpDllPath)" #> -<#@ assembly name="$(SonarVBDllPath)" #> -<#@ assembly name="$(EnterpriseSonarCSharpDllPath)" #> -<#@ assembly name="$(EnterpriseSonarVBDllPath)" #> -<#@ assembly name="$(MSCodeAnalysisCommonDllPath)" #> -<#@ assembly name="$(MSCodeAnalysisWorkspacesDllPath)" #> -<#@ assembly name="$(SystemCompositionAttributedModelDllPath)" #> -<#@ assembly name="netstandard" #> -<#@ assembly name="System.Runtime" #> -<#@ import namespace="System.Linq" #> -<#@ import namespace="System.Reflection" #> -<#@ import namespace="System.Collections.Generic" #> -<#@ import namespace="SonarAnalyzer" #> -<#@ import namespace="Microsoft.CodeAnalysis" #> -<#@ import namespace="Microsoft.CodeAnalysis.Diagnostics" #> -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using Microsoft.CodeAnalysis; -using System; -using System.Collections.Immutable; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions -{ - /// - /// Generated class that returns SupportedSuppressions for all Sonar C# and VB.NET rules - /// - internal sealed class SupportedSuppressionsBuilder - { - private static readonly Lazy lazy = new Lazy(() => new SupportedSuppressionsBuilder()); - - public static SupportedSuppressionsBuilder Instance => lazy.Value; - - public ImmutableArray Descriptors { get; } - - private SupportedSuppressionsBuilder() - { - Descriptors = GetDescriptors(); - } - - private static ImmutableArray GetDescriptors() - { - var descriptors = new SuppressionDescriptor[] - { - // ************************************************************************************************* - // If the number of diagnostic ids changes significantly or decreases when the analysers are updated, - // investigate! There may be a problem with code generator. - // ************************************************************************************************* -<# - System.AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; - - var idList = GetFilteredDiagnosticIds(); - WriteLine(" // Number of unique diagnostic ids (C# and VB.NET): " + idList.Count()); - foreach(var id in idList) - { -#> - CreateDescriptor("<#= id #>"), -<# - } -#> - }; - return ImmutableArray.ToImmutableArray(descriptors); - } - - private static SuppressionDescriptor CreateDescriptor(string diagId) => - new SuppressionDescriptor("X" + diagId, diagId, "Suppressed on the Sonar server"); - } -} -<#+ - -// T4 Text Templates -// MS docs - see https://docs.microsoft.com/en-us/visualstudio/modeling/code-generation-and-t4-text-templates?view=vs-2022 -// -// Notes on debugging through the generator -// ---------------------------------------- -// * change the "debug" value in line 1 to "true" -// * add a call to "System.Diagnostics.Debugger.Launch();" somewhere in the code below -// * save this file. -// A dialogue should pop up asking you attach a debugger. -// -// -// Notes on resolving Roslyn assemblies -// ------------------------------------ -// The T4 transformation can be done inside VS (by saving the .tt file, or right-clicking on it in the Solution Explorer -// and selecting "Run Custom Tool"). When run that way, the transformation is hosted in-memory in VS, and it uses its -// versions of the Roslyn assemblies. It also uses the binding redirects in devenv.exe.config to forward references from -// older versions of Roslyn to the VS versions. -// -// When the transformation is done at build time (whether building in VS or outside), the transformation is hosted by -// MSBuild, which locates and resolves the assemblies separately. -// To make the transformation work in MSBuild, we need to: -// 1) reference some Roslyn NuGet packages required during the transformation, -// 2) handling the binding redirects in code (using the AssemblyResolve event), and -// 3) use Reflection to create the analyzer instances and extract the analyzer ids. - - private System.Reflection.Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) - { - // Simple binding redirect - try a simple match based on name, ignoring the version. - // This approximates to what VS is doing when redirecting the Roslyn assemblies. - var requiredAsm = new System.Reflection.AssemblyName(args.Name); - return System.AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(x => x.GetName().Name == requiredAsm.Name); - } - - // The set of assemblies in which we should look for diagnostics - private readonly Assembly[] AssembliesWithDiagnostics = new Assembly[] - { - typeof(SonarAnalyzer.CSharp.Rules.CommentKeyword).Assembly, - typeof(SonarAnalyzer.Enterprise.CSharp.Rules.SymbolicExecutionRunner).Assembly, - typeof(SonarAnalyzer.VisualBasic.Rules.CommentKeyword).Assembly, - typeof(SonarAnalyzer.Enterprise.VisualBasic.Rules.SymbolicExecutionRunner).Assembly - }; - - private IEnumerable GetAllDiagnosticIds() => - AssembliesWithDiagnostics.SelectMany(asm => asm.GetTypes()) - .SelectMany(type => GetIdsUsingReflection(type)); - private IEnumerable GetFilteredDiagnosticIds() - { - return GetAllDiagnosticIds() - .Distinct() - .Where(id => !id.StartsWith("S999")) // exclude "utility" analyzers - .OrderBy(x => x); - } - - private IEnumerable GetIdsUsingReflection(Type t) - { - try - { - if (IsDiagnosticAnalyzer(t)) - { - var analyzer = Create(t); - return GetDiagnosticIds(analyzer); - } - } - catch(Exception e) - { - WriteLine($"[ERROR] Error creating diagnostic. {t.FullName}, Error: {e.Message}"); - } - return Array.Empty(); - } - - private static object Create(Type t) => - Activator.CreateInstance(t); - - private bool IsDiagnosticAnalyzer(Type t) - { - if (t.IsAbstract) { return false; } - return InheritsFromDiagnosticAnalyzer(t) && t.GetCustomAttributes(true).Any(x => x.GetType().Name == "DiagnosticAnalyzerAttribute"); - } - - private static bool InheritsFromDiagnosticAnalyzer(Type t) - { - // Note: there are some symbolic execution classes that have the [DiagnosticAnalyzer] attribute - // but that do not inherit from DiagnosticAnalyzer - // e.g. https://github.com/SonarSource/sonar-dotnet/blob/aab23c510aa525565c01658418a4c7affa29d17a/analyzers/src/SonarAnalyzer.CSharp/SymbolicExecution/Roslyn/CalculationsShouldNotOverflow.cs#L23 - // See bug https://github.com/SonarSource/sonarlint-visualstudio/issues/4467 - if (t == null) { return false; } - if (t.FullName == "Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer") { return true; } - return InheritsFromDiagnosticAnalyzer(t.BaseType); - } - - private IEnumerable GetDiagnosticIds(object analyzerInstance) - { - var propertyInfo = analyzerInstance.GetType().GetProperty("SupportedDiagnostics"); - var supportedDiagnostics = propertyInfo.GetValue(analyzerInstance) as IEnumerable; - - return supportedDiagnostics.Select(GetIdFromDescriptor); - } - - private string GetIdFromDescriptor(object diagnosticDescriptor) - { - var propertyInfo = diagnosticDescriptor.GetType().GetProperty("Id"); - return propertyInfo.GetValue(diagnosticDescriptor) as string; - } -#> \ No newline at end of file diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/SuppressionChecker.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/SuppressionChecker.cs deleted file mode 100644 index 0774940846..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/SuppressionChecker.cs +++ /dev/null @@ -1,142 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Linq; -using Microsoft.CodeAnalysis; -using SonarLint.VisualStudio.Core.Helpers; -using SonarLint.VisualStudio.Roslyn.Suppressions.Settings.Cache; -using SonarQube.Client; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions -{ - internal interface ISuppressionChecker - { - /// - /// Returns true if the diagnostic should be suppressed, otherwise false - /// - bool IsSuppressed(Diagnostic reportedDiagnostic, string settingsKey); - } - - internal class SuppressionChecker : ISuppressionChecker - { - private readonly ISettingsCache settingsCache; - private readonly IChecksumCalculator checksumCalculator; - - public SuppressionChecker(ISettingsCache settingsCache) - : this(settingsCache, new ChecksumCalculator()) - { - } - - internal SuppressionChecker(ISettingsCache settingsCache, IChecksumCalculator checksumCalculator) - { - this.settingsCache = settingsCache; - this.checksumCalculator = checksumCalculator; - } - - public bool IsSuppressed(Diagnostic reportedDiagnostic, string settingsKey) - { - // Can't match issues that don't have a location in source - if (reportedDiagnostic.Location == null || reportedDiagnostic.Location.Kind != LocationKind.SourceFile) - { - return false; - } - - var settings = settingsCache.GetSettings(settingsKey); - var suppressedIssues = settings.Suppressions; - - if (suppressedIssues == null || !suppressedIssues.Any()) - { - return false; - } - - // TODO: consider perf. This initial implementation is very inefficient: - // * it loops through every issue - // * the hash of the Roslyn issue could be calculate multiple times. - bool matchFound = suppressedIssues.Any(x => IsMatch(reportedDiagnostic, x, checksumCalculator)); - return matchFound; - } - - internal static /* for testing */ bool IsMatch(Diagnostic diagnostic, SuppressedIssue suppressedIssue, IChecksumCalculator checksumCalculator) - { - // Criteria for matching a Roslyn issue to an issue from the server: - // (1) same issue key - // (2) same file - // (3) same line number OR same line hash (to take account of edits in the file since it was analysed) - - // Note: this first implementation is written in a verbose style to make it easier to debug. - - // (1) Id - if (!StringComparer.OrdinalIgnoreCase.Equals(diagnostic.Id, suppressedIssue.RoslynRuleId)) - { - return false; - } - - // (2) File - if (!IsSameFile(diagnostic, suppressedIssue)) - { - return false; - } - - // (3) Location - line matches - var roslynLineSpan = diagnostic.Location.GetLineSpan(); - if (IsSameLine(roslynLineSpan, suppressedIssue)) - { - return true; - } - - // (3) Location - hash (most expensive check) - var syntaxTree = diagnostic.Location.SourceTree; - // TODO: check why the existing RoslynLiveIssueFactory.Create is using the EndLine of the issue - // rather than the StartLine - var lineText = syntaxTree.GetText().Lines[roslynLineSpan.EndLinePosition.Line].ToString(); - var roslynHash = checksumCalculator.Calculate(lineText); - - if (StringComparer.Ordinal.Equals(roslynHash, suppressedIssue.Hash)) - { - return true; - } - - return false; - } - - private static bool IsSameFile(Diagnostic diagnostic, SuppressedIssue suppressedIssue) => - PathHelper.IsServerFileMatch(diagnostic.Location.SourceTree?.FilePath, suppressedIssue.FilePath); - - private static bool IsSameLine(FileLinePositionSpan roslynLineSpan, SuppressedIssue suppressedIssue) - { - // Special case: file-level issues - var roslynFileLevelIssue = IsRoslynFileLevelIssue(roslynLineSpan); - var sonarFileLevelIssue = !suppressedIssue.RoslynIssueLine.HasValue; - if (sonarFileLevelIssue || roslynFileLevelIssue) - { - // File-level issues can only match other file-level issues - return (sonarFileLevelIssue && roslynFileLevelIssue); - } - - return suppressedIssue.RoslynIssueLine.Value == roslynLineSpan.StartLinePosition.Line; - } - - private static bool IsRoslynFileLevelIssue(FileLinePositionSpan lineSpan) => lineSpan.StartLinePosition.Line == 0 && - lineSpan.StartLinePosition.Character == 0 && - lineSpan.EndLinePosition.Line == 0 && - lineSpan.EndLinePosition.Character == 0; - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/SuppressionExecutionContext.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/SuppressionExecutionContext.cs deleted file mode 100644 index 5809d6b542..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/SuppressionExecutionContext.cs +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Text.RegularExpressions; -using Microsoft.CodeAnalysis.Diagnostics; -using SonarLint.VisualStudio.Core; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions -{ - public interface ISuppressionExecutionContext - { - bool IsInConnectedMode { get; } - string SettingsKey { get; } - } - internal class SuppressionExecutionContext : ISuppressionExecutionContext - { - private const string Exp = @"\\SonarLint for Visual Studio\\Bindings\\(?[^\\/]+)\\(CSharp|VB)\\SonarLint.xml$"; - private static readonly Regex SonarLintFileRegEx = new Regex(Exp, - RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.CultureInvariant, - RegexConstants.DefaultTimeout); - - public SuppressionExecutionContext(AnalyzerOptions analyzerOptions) - { - GetProjectKey(analyzerOptions); - } - - private void GetProjectKey(AnalyzerOptions analyzerOptions) - { - foreach (var item in analyzerOptions.AdditionalFiles) - { - var match = SonarLintFileRegEx.Match(item.Path); - if (match.Success) - { - SettingsKey = match.Groups["solutionName"].Value; - return; - } - } - } - - public bool IsInConnectedMode => SettingsKey != null; - - public string SettingsKey { get; private set; } = null; - - public string Mode => IsInConnectedMode ? "Connected" : "Standalone"; - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/SuppressorCodeGen.targets b/src/Roslyn.Suppressions/Roslyn.Suppressions/SuppressorCodeGen.targets deleted file mode 100644 index 9478e957a0..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/SuppressorCodeGen.targets +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - $(EnterpriseDotnetAnalyzersExtractDir)\SonarAnalyzer.CSharp.dll - $(EnterpriseDotnetAnalyzersExtractDir)\SonarAnalyzer.Enterprise.CSharp.dll - $(EnterpriseDotnetAnalyzersExtractDir)\SonarAnalyzer.VisualBasic.dll - $(EnterpriseDotnetAnalyzersExtractDir)\SonarAnalyzer.Enterprise.VisualBasic.dll - - $(PkgMicrosoft_CodeAnalysis_Common)\lib\netstandard2.0\Microsoft.CodeAnalysis.dll - $(PkgMicrosoft_CodeAnalysis_Workspaces_Common)\lib\netstandard2.0\Microsoft.CodeAnalysis.Workspaces.dll - $(PkgSystem_Composition_AttributedModel)\lib\netstandard1.0\System.Composition.AttributedModel.dll - - - - false - true - - - - - - $(SonarCSharpDllPath) - false - - - $(SonarVBDllPath) - false - - - $(EnterpriseSonarCSharpDllPath) - false - - - $(EnterpriseSonarVBDllPath) - false - - - $(MSCodeAnalysisCommonDllPath) - false - - - $(MSCodeAnalysisWorkspacesDllPath) - false - - - $(SystemCompositionAttributedModelDllPath) - false - - - - - - TextTemplatingFileGenerator - SupportedSuppressionBuilder.g.cs - - - True - True - SupportedSuppressionBuilder.tt - - - - - - - - - diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/packages.lock.json b/src/Roslyn.Suppressions/Roslyn.Suppressions/packages.lock.json deleted file mode 100644 index 17b4e630d2..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/packages.lock.json +++ /dev/null @@ -1,1292 +0,0 @@ -{ - "version": 1, - "dependencies": { - ".NETFramework,Version=v4.7.2": { - "Microsoft.CodeAnalysis.Common": { - "type": "Direct", - "requested": "[3.11.0, )", - "resolved": "3.11.0", - "contentHash": "FDKSkRRXnaEWMa2ONkLMo0ZAt/uiV1XIXyodwKIgP1AMIKA7JJKXx/OwFVsvkkUT4BeobLwokoxFw70fICahNg==", - "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.3.2", - "System.Collections.Immutable": "5.0.0", - "System.Memory": "4.5.4", - "System.Reflection.Metadata": "5.0.0", - "System.Runtime.CompilerServices.Unsafe": "5.0.0", - "System.Text.Encoding.CodePages": "4.5.1", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "Microsoft.CodeAnalysis.Workspaces.Common": { - "type": "Direct", - "requested": "[3.11.0, )", - "resolved": "3.11.0", - "contentHash": "YAbH4LCJfh8DhDGwYzSHqvnF06lKkVwblr8C+GwIYCv0i3Rzqjnbversat+i2n9k8twQ43yxVGTYK5p/mIOj4w==", - "dependencies": { - "Humanizer.Core": "2.2.0", - "Microsoft.Bcl.AsyncInterfaces": "5.0.0", - "Microsoft.CodeAnalysis.Common": "[3.11.0]", - "System.Composition": "1.0.31", - "System.IO.Pipelines": "5.0.1" - } - }, - "Microsoft.VisualStudio.SDK": { - "type": "Direct", - "requested": "[17.0.31902.203, )", - "resolved": "17.0.31902.203", - "contentHash": "YSOMLjLm0k4Q5JeoPvyRrYaHdUzxkKtkt9Hn/0NjWDdQ7tLrhZjR3SOiaSMYOFYQCQ0G7v2UBIRHfbGAnt7SNg==", - "dependencies": { - "Microsoft.VisualStudio.CommandBars": "17.0.31902.203", - "Microsoft.VisualStudio.ComponentModelHost": "17.0.487", - "Microsoft.VisualStudio.CoreUtility": "17.0.487", - "Microsoft.VisualStudio.Debugger.Interop.12.0": "17.0.31902.203", - "Microsoft.VisualStudio.Debugger.Interop.14.0": "17.0.31902.203", - "Microsoft.VisualStudio.Debugger.Interop.15.0": "17.0.31902.203", - "Microsoft.VisualStudio.Debugger.Interop.16.0": "17.0.31902.203", - "Microsoft.VisualStudio.Designer.Interfaces": "17.0.31902.203", - "Microsoft.VisualStudio.Editor": "17.0.487", - "Microsoft.VisualStudio.ImageCatalog": "17.0.31902.203", - "Microsoft.VisualStudio.Imaging": "17.0.31902.203", - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31902.203", - "Microsoft.VisualStudio.Interop": "17.0.31902.203", - "Microsoft.VisualStudio.Language": "17.0.487", - "Microsoft.VisualStudio.Language.Intellisense": "17.0.487", - "Microsoft.VisualStudio.Language.NavigateTo.Interfaces": "17.0.487", - "Microsoft.VisualStudio.Language.StandardClassification": "17.0.487", - "Microsoft.VisualStudio.LanguageServer.Client": "17.0.5158", - "Microsoft.VisualStudio.OLE.Interop": "17.0.31902.203", - "Microsoft.VisualStudio.Package.LanguageService.15.0": "17.0.31902.203", - "Microsoft.VisualStudio.ProjectAggregator": "17.0.31902.203", - "Microsoft.VisualStudio.Setup.Configuration.Interop": "3.0.4492", - "Microsoft.VisualStudio.Shell.Design": "17.0.31902.203", - "Microsoft.VisualStudio.Shell.Interop": "17.0.31902.203", - "Microsoft.VisualStudio.Shell.Interop.10.0": "17.0.31902.203", - "Microsoft.VisualStudio.Shell.Interop.11.0": "17.0.31902.203", - "Microsoft.VisualStudio.Shell.Interop.12.0": "17.0.31902.203", - "Microsoft.VisualStudio.Shell.Interop.8.0": "17.0.31902.203", - "Microsoft.VisualStudio.Shell.Interop.9.0": "17.0.31902.203", - "Microsoft.VisualStudio.TaskRunnerExplorer.14.0": "14.0.0", - "Microsoft.VisualStudio.Text.Logic": "17.0.487", - "Microsoft.VisualStudio.TextManager.Interop": "17.0.31902.203", - "Microsoft.VisualStudio.TextManager.Interop.10.0": "17.0.31902.203", - "Microsoft.VisualStudio.TextManager.Interop.11.0": "17.0.31902.203", - "Microsoft.VisualStudio.TextManager.Interop.12.0": "17.0.31902.203", - "Microsoft.VisualStudio.TextManager.Interop.8.0": "17.0.31902.203", - "Microsoft.VisualStudio.TextManager.Interop.9.0": "17.0.31902.203", - "Microsoft.VisualStudio.TextTemplating.VSHost": "17.0.31902.203", - "Microsoft.VisualStudio.VCProjectEngine": "17.0.31902.203", - "Microsoft.VisualStudio.VSHelp": "17.0.31902.203", - "Microsoft.VisualStudio.VSHelp80": "17.0.31902.203", - "Microsoft.VisualStudio.Validation": "17.0.28", - "Microsoft.VisualStudio.WCFReference.Interop": "17.0.31902.203", - "Microsoft.VisualStudio.Web.BrowserLink.12.0": "12.0.0", - "Microsoft.Win32.Primitives": "4.3.0", - "System.ComponentModel.Composition": "4.5.0", - "VSLangProj": "17.0.31902.203", - "VSLangProj100": "17.0.31902.203", - "VSLangProj110": "17.0.31902.203", - "VSLangProj140": "17.0.31902.203", - "VSLangProj150": "17.0.31902.203", - "VSLangProj157": "17.0.31902.203", - "VSLangProj158": "17.0.31902.203", - "VSLangProj165": "17.0.31902.203", - "VSLangProj2": "17.0.31902.203", - "VSLangProj80": "17.0.31902.203", - "VSLangProj90": "17.0.31902.203", - "envdte": "17.0.31902.203", - "envdte100": "17.0.31902.203", - "envdte80": "17.0.31902.203", - "envdte90": "17.0.31902.203", - "envdte90a": "17.0.31902.203", - "stdole": "17.0.31902.203" - } - }, - "Newtonsoft.Json": { - "type": "Direct", - "requested": "[13.0.3, )", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, - "System.Composition.AttributedModel": { - "type": "Direct", - "requested": "[1.0.31, )", - "resolved": "1.0.31", - "contentHash": "NHWhkM3ZkspmA0XJEsKdtTt1ViDYuojgSND3yHhTzwxepiwqZf+BCWuvCbjUt4fe0NxxQhUDGJ5km6sLjo9qnQ==" - }, - "System.IO.Abstractions": { - "type": "Direct", - "requested": "[9.0.4, )", - "resolved": "9.0.4", - "contentHash": "1h4krG51ZiW/CGzM8gtqrRW2oeG6WZDfPaj27suexL8PxBVahsUlUKMJrqI4kkh6ggHLSDd7MFeU8orpk6COZg==" - }, - "BouncyCastle.Cryptography": { - "type": "Transitive", - "resolved": "2.4.0", - "contentHash": "SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==" - }, - "envdte": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "AC6jYeSnDnYZEs5nHKEtBupRWAQxriX2X3M25HyJlU9cvCCqPCByMwIbvlz8kXk+GfGxSL8sN+YOihU2SvrjXw==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "envdte100": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "XICXfPHF4SQzqpUtQgXZsuSTyYOdSOymPMUqH/Q6QBrpEoMiJxmW/eEvLwgqJqiW5+HaajJImlVJkrnZJ4fjfg==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "envdte80": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "79xAQpQmKqNnBWtH96Fm+onXsCS7ZTK0CfhCIe3BC56whCMwyh52M/+Qj/xOGhbFnPpjEzJhPnoL1jppniyRAw==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "envdte90": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "AMFd0yjzXUV26i0Yzr5MBdolXtz92NZWUvacohGRFFHIHaa8BkKl1c40nw9m33l4cXcwMECsvPkhAcfietYkQg==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "envdte90a": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "8rZmZEBu8uDMF1Fq1CITa540cJA8lVj9d9B2D2IgDL6heGkBfQc1nBf2BRcJT81SS9c0wEI1VLIVe3WSPeYG5g==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, - "Humanizer.Core": { - "type": "Transitive", - "resolved": "2.2.0", - "contentHash": "rsYXB7+iUPP8AHgQ8JP2UZI2xK2KhjcdGr9E6zX3CsZaTLCaw8M35vaAJRo1rfxeaZEVMuXeaquLVCkZ7JcZ5Q==" - }, - "MessagePack": { - "type": "Transitive", - "resolved": "2.2.85", - "contentHash": "3SqAgwNV5LOf+ZapHmjQMUc7WDy/1ur9CfFNjgnfMZKCB5CxkVVbyHa06fObjGTEHZI7mcDathYjkI+ncr92ZQ==", - "dependencies": { - "MessagePack.Annotations": "2.2.85", - "Microsoft.Bcl.AsyncInterfaces": "1.0.0", - "System.Collections.Immutable": "1.5.0", - "System.Memory": "4.5.3", - "System.Reflection.Emit": "4.6.0", - "System.Reflection.Emit.Lightweight": "4.6.0", - "System.Runtime.CompilerServices.Unsafe": "4.5.2", - "System.Threading.Tasks.Extensions": "4.5.3" - } - }, - "MessagePack.Annotations": { - "type": "Transitive", - "resolved": "2.2.85", - "contentHash": "YptRsDCQK35K5FhmZ0LojW4t8I6DpetLfK5KG8PVY2f6h7/gdyr8f4++xdSEK/xS6XX7/GPvEpqszKVPksCsiQ==" - }, - "Microsoft.Alm.Authentication": { - "type": "Transitive", - "resolved": "4.0.0.1", - "contentHash": "G+EwnnZeoee3McrWZQ0TOtQShJyCzFwhJdEXIbz5t+G2e67KICipAHfflDSYwokB43YK+y5J7ArnRHDLYtNliA==", - "dependencies": { - "Microsoft.IdentityModel.Clients.ActiveDirectory": "3.13.8" - } - }, - "Microsoft.Bcl.AsyncInterfaces": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "W8DPQjkMScOMTtJbPwmPyj9c3zYSFGawDW3jwlBOOsnY+EzZFLgNQ/UMkK35JmkNOVPdCyPr2Tw7Vv9N+KA3ZQ==", - "dependencies": { - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "Microsoft.Build.Framework": { - "type": "Transitive", - "resolved": "16.5.0", - "contentHash": "K0hfdWy+0p8DJXxzpNc4T5zHm4hf9QONAvyzvw3utKExmxRBShtV/+uHVYTblZWk+rIHNEHeglyXMmqfSshdFA==" - }, - "Microsoft.CodeAnalysis.Analyzers": { - "type": "Transitive", - "resolved": "3.3.2", - "contentHash": "7xt6zTlIEizUgEsYAIgm37EbdkiMmr6fP6J9pDoKEpiGM4pi32BCPGr/IczmSJI9Zzp0a6HOzpr9OvpMP+2veA==" - }, - "Microsoft.CSharp": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA==" - }, - "Microsoft.IdentityModel.Clients.ActiveDirectory": { - "type": "Transitive", - "resolved": "3.13.8", - "contentHash": "RAYmpEVVnl8qWdPmm62k9wm6MUB9H4Gk3l7uBADDCCQJDOkpTAINzgWHTBF8hkaTHFLaGIfgOeeLcKT+K1PG7A==" - }, - "Microsoft.NETCore.Platforms": { - "type": "Transitive", - "resolved": "1.1.1", - "contentHash": "TMBuzAHpTenGbGgk0SMTwyEkyijY/Eae4ZGsFNYJvAr/LDn1ku3Etp3FPxChmDp5HHF3kzJuoaa08N0xjqAJfQ==" - }, - "Microsoft.NETCore.Targets": { - "type": "Transitive", - "resolved": "1.1.3", - "contentHash": "3Wrmi0kJDzClwAC+iBdUBpEKmEle8FQNsCs77fkiOIw/9oYA07bL1EZNX0kQ2OMN3xpwvl0vAtOCYY3ndDNlhQ==" - }, - "Microsoft.ServiceHub.Analyzers": { - "type": "Transitive", - "resolved": "3.0.3078", - "contentHash": "LQsmEP/5i9PvM6O1dx69Yj3C0z/tSWiaLjoX31jQ+ilJZ8x7yqthYOnWaQpeZKxJn+oFxymzGtXgPasnqYM/ww==" - }, - "Microsoft.ServiceHub.Client": { - "type": "Transitive", - "resolved": "3.0.3078", - "contentHash": "hYqQlgUhnTq7VHYfIBvuWCwAiTjqhCfEX7d/ISVtEGEv7/N89QAbL+0XCz2NZRN6yMDtVMEoee5Q4k6/uwWlJg==", - "dependencies": { - "Microsoft.ServiceHub.Framework": "3.0.3078", - "Microsoft.ServiceHub.Resources": "3.0.3078", - "Microsoft.VisualStudio.RemoteControl": "16.3.32", - "Microsoft.VisualStudio.Telemetry": "16.3.176", - "StreamJsonRpc": "2.7.70", - "System.Collections.Immutable": "5.0.0" - } - }, - "Microsoft.ServiceHub.Framework": { - "type": "Transitive", - "resolved": "3.0.3078", - "contentHash": "RMBx+TEE3Fl6CRd1d1ZWKnNPRbPL23NFydDEEjRtZdwTSWe1x0gkUqnGU/ZgtqSsgWUfaQtEPxd8S9qfPGkz0Q==", - "dependencies": { - "Microsoft.ServiceHub.Analyzers": "3.0.3078", - "StreamJsonRpc": "2.7.70", - "System.Collections.Immutable": "5.0.0" - } - }, - "Microsoft.ServiceHub.Resources": { - "type": "Transitive", - "resolved": "3.0.3078", - "contentHash": "02mGIKyVfnXFEeicpV2RbZapHd6vcefFSSZvjAA+O0kWgB9x2D5Pd3M94Il9LiLgFnw3mmxtf68tbEjOhQ0rWg==" - }, - "Microsoft.VisualStudio.CommandBars": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "+eBuWROC04s6NyVhJW/GJIqy8gOhFaxkWiHtuEf2bVTGkKQ/gx/Rjfegj79H/xEPnBmvbKp8e5+dxJ1WA4r1XA==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.ComponentModelHost": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "mIvfzFYYtPbf+aZRLjtOmMfksKPGML2U2z7RqHJ+m0AVTumssJbOpaD5gOgYcUvE6+AjFoUpg+NLVjwxdZnVYQ==", - "dependencies": { - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31723.112", - "Microsoft.VisualStudio.Interop": "17.0.31723.112", - "System.ComponentModel.Composition": "4.5.0" - } - }, - "Microsoft.VisualStudio.CoreUtility": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "qk4BeMGWIklu4ia8zf6JPLUGXuJcbjhqfYhIHYbDKETmGxRHoNsvzGBIZT8GkS4VSjcibOFnbCowJfNWy5TfhQ==", - "dependencies": { - "Microsoft.VisualStudio.Threading": "17.0.63", - "System.Collections.Immutable": "5.0.0", - "System.ComponentModel.Composition": "4.5.0" - } - }, - "Microsoft.VisualStudio.Debugger.Interop.10.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "YipUF2uyw4MH3WFZf+/4lC9jcHjkL8GgPdwvH800x+kVHAAVIEXJTqPTmsJnBrT/QqXdPHQrM6Srae0VQjG3gA==", - "dependencies": { - "Microsoft.VisualStudio.Debugger.InteropA": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Debugger.Interop.11.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "NENWpbG7TvdF1TwesraECA+PgprCLR3Mfts7t8fL5KGc59QiRCeZTpVWBzotvRLMQ0lhA0VYYh1ErDV9D+LM2g==", - "dependencies": { - "Microsoft.VisualStudio.Debugger.InteropA": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Debugger.Interop.12.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "Os4TTwz9mrtS5AzTKMyEr2NPIJmmBGlf+Cn4eFPn6t4yMfDYfaFRnOtDgpiF7IyduB+mywO8v+wMLWiQuJth9Q==", - "dependencies": { - "Microsoft.VisualStudio.Debugger.InteropA": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Debugger.Interop.14.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "UwQukQ2/HdIxKqHGDQ8oipxrJ4IWLdQfBBuY8Yl61SAVhcGzDP4Z/XkB7RSIbJOAeFDzu/kuKZ3zXR7jXazAJA==", - "dependencies": { - "Microsoft.VisualStudio.Debugger.Interop.11.0": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Debugger.Interop.15.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "P7C071a0dx7TP9NEBmt8pR3BZMOIqQYNWYW1pvxaY6jOb99/q7sm/KgYDKGrK/HEBefbn6g9T6hrVlUeP2GsWQ==", - "dependencies": { - "Microsoft.VisualStudio.Debugger.Interop.10.0": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Debugger.Interop.16.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "eYE5pOBbWacuf0pUyQHJH9LuP15x67CUy8VJv9AeJM3E91Y95Ff5hkCasBZtf9TJ9uMVnx0+6PdZLkZdYcS4Yg==", - "dependencies": { - "Microsoft.VisualStudio.Debugger.Interop.10.0": "17.0.31902.203", - "Microsoft.VisualStudio.Debugger.Interop.11.0": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Debugger.InteropA": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "y1CLio4/zI5PYWt760mJhduq/gpNZNrDwX2c2kBB5p+D8a1ExH4d2tNDTqbQdciOiNR47YenAcSUpyfHv9pNkg==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Designer.Interfaces": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "eeNZCUX6RJGnc6YC6Bfs8qbEVt7WxWRPXEbEBjyOqCDU4dJPkR1OLVWMUHkA4XM3sXKImv1LmAwHZk3cWK5iGQ==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Editor": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "UuIMD8xLAkvwuszD7jlc4ADRckr59eyJ7YJFepTtja2IDMzt5SAGa0kxxjUQ3Zeg0KuhuwcZBwuYmSCgx5YSjA==", - "dependencies": { - "Microsoft.VisualStudio.CoreUtility": "17.0.487", - "Microsoft.VisualStudio.GraphModel": "17.0.31723.112", - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31723.112", - "Microsoft.VisualStudio.Interop": "17.0.31723.112", - "Microsoft.VisualStudio.Language": "17.0.487", - "Microsoft.VisualStudio.ProjectAggregator": "17.0.31723.112", - "Microsoft.VisualStudio.RpcContracts": "17.0.51", - "Microsoft.VisualStudio.Shell.15.0": "17.0.31723.112", - "Microsoft.VisualStudio.Text.Data": "17.0.487", - "Microsoft.VisualStudio.Text.Logic": "17.0.487", - "Microsoft.VisualStudio.Text.UI": "17.0.487", - "Microsoft.VisualStudio.Text.UI.Wpf": "17.0.487", - "Microsoft.VisualStudio.Threading": "17.0.63", - "Microsoft.VisualStudio.Validation": "17.0.28" - } - }, - "Microsoft.VisualStudio.GraphModel": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "53cv+WBrXiX3Bomk2W+gz74tFqFa54OvJ++u4RYAdntvXb/hx+m1EhBgtHKOFu057rVxAq/YNFY7vLCUusQGoQ==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203", - "System.ComponentModel.Composition": "4.5.0" - } - }, - "Microsoft.VisualStudio.ImageCatalog": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "nNfMuMiuC0UX/r5ryjIuqWyj/Wzrz63wuEcX70NtICUNmm/Bc5blXmgH7Et+ArVzZlXi2ExNqbLAi60IpNdaYw==", - "dependencies": { - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31902.203", - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Imaging": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "NCJX5aUZxcj32Al5E9PI2XONEjR8gQyzjGL2NfNxw8PzDPd6yMmU96y7rnvv0dclPUw0gCcVMtLnNwpCaeXBdg==", - "dependencies": { - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31902.203", - "Microsoft.VisualStudio.Threading": "17.0.64", - "Microsoft.VisualStudio.Utilities": "17.0.31902.203", - "System.Collections.Immutable": "5.0.0" - } - }, - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "d9I8RzOeF9z1L/QJ3nWOMvJphsxe31cU7WBZW7aM0VTWZUWcYwYQH0CZWFPZsXgFWWRgiK5WL7FiMnYJfeDutw==" - }, - "Microsoft.VisualStudio.Interop": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "KWBjmU85KpWJiqRgkvuJfQgW8/1RtkFnJ4e4EIcoXYHab/1tinXv219midxHJoc6Sa6E/7Uo3Ku4bWBQMFE9dA==" - }, - "Microsoft.VisualStudio.Language": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "j8hL3Wwst8hwstbBzY24WV0PWYkcWDAC/Y2JHdO9jdlBjvHCHUPN4zNtu6A9oCTNsyLMxzK+pq+X45FFVTeqrA==", - "dependencies": { - "Microsoft.VisualStudio.CoreUtility": "17.0.487", - "Microsoft.VisualStudio.Text.Data": "17.0.487", - "Microsoft.VisualStudio.Text.Logic": "17.0.487", - "Microsoft.VisualStudio.Text.UI": "17.0.487", - "Newtonsoft.Json": "13.0.1", - "StreamJsonRpc": "2.8.28", - "System.Collections.Immutable": "5.0.0", - "System.ComponentModel.Composition": "4.5.0", - "System.Private.Uri": "4.3.2" - } - }, - "Microsoft.VisualStudio.Language.Intellisense": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "iPi01Ep2YM4rgL6z26s4eEy8hOYwt5jE1aLebknCnBzUAB3avrSVa1AcOo726SdVN8h/h0lVRE4LMrC4WAQ4rQ==", - "dependencies": { - "Microsoft.VisualStudio.CoreUtility": "17.0.487", - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31723.112", - "Microsoft.VisualStudio.Language": "17.0.487", - "Microsoft.VisualStudio.Text.Data": "17.0.487", - "Microsoft.VisualStudio.Text.Logic": "17.0.487", - "Microsoft.VisualStudio.Text.UI": "17.0.487", - "System.Runtime.CompilerServices.Unsafe": "5.0.0" - } - }, - "Microsoft.VisualStudio.Language.NavigateTo.Interfaces": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "5/3AstEbNcaZtwwONLcHXc2Nzels7j89jzXsYsxPQCSW9inbDbQoBC6+wtsAwKxOu1BeKpPaGi7bmc7Y+R+vSA==", - "dependencies": { - "Microsoft.VisualStudio.Imaging": "17.0.31723.112", - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31723.112", - "Microsoft.VisualStudio.Interop": "17.0.31723.112", - "Microsoft.VisualStudio.Text.Logic": "17.0.448", - "Microsoft.VisualStudio.Utilities": "17.0.31723.112" - } - }, - "Microsoft.VisualStudio.Language.StandardClassification": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "n7l7tLV69+FtdH+/SPR8MdVc3UsAvlrgE38mMlXtW3dkxx5BYnSiHl93vZ4omDbX45Rzu9b4DDR1HMzg3GaKzQ==", - "dependencies": { - "Microsoft.VisualStudio.Text.Logic": "17.0.487" - } - }, - "Microsoft.VisualStudio.LanguageServer.Client": { - "type": "Transitive", - "resolved": "17.0.5158", - "contentHash": "cZNQHkvjpCnEnxZrLBflAAXc8CwJYMUgDEcCTgrY86Zpa6tHfaHpMoK3aBXtBlzlJ25vjeqBoilXbbCrPrHnJg==", - "dependencies": { - "Microsoft.VisualStudio.CoreUtility": "17.0.448", - "Microsoft.VisualStudio.Shell.15.0": "17.0.31723.112", - "Microsoft.VisualStudio.Utilities": "17.0.31723.112", - "Microsoft.VisualStudio.Validation": "17.0.28", - "StreamJsonRpc": "2.8.28" - } - }, - "Microsoft.VisualStudio.OLE.Interop": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "lhZX3Vxm+bRP7/lb8zYG2DlLx3Ea1tnatV+SUYAJ+sL8Qr7v8VBmZurPxGsKYh5Q7ePNjOlkWk2eSaK6TaxUyg==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Package.LanguageService.15.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "ZR8l9shqIHXGzfd4Wx9H7Bkgd4ydsMC+Sh8wk05jOV3qS9j9cYTNkRKXOny9rK7lZJIQcH8fPYpIe48kAwZPmg==", - "dependencies": { - "Microsoft.VisualStudio.Shell.Framework": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.ProjectAggregator": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "C96eQGjS87DX1yjl/C57YFpWX2hD2Vi0BlLnz44nFhAxCvQ2Fy5FN/E63yNMIdX+iAZkCoG80c1D7C6fBOL0XA==" - }, - "Microsoft.VisualStudio.RemoteControl": { - "type": "Transitive", - "resolved": "16.3.41", - "contentHash": "Q9lz2anDPJxDLznQRaybv21aY3qgQJmGJiUonH8z2D0XAgKMlMelsu9bg9zhnKCxtA/jreRAM3Md2W6thiDOwQ==", - "dependencies": { - "Microsoft.VisualStudio.Utilities.Internal": "16.3.23" - } - }, - "Microsoft.VisualStudio.RpcContracts": { - "type": "Transitive", - "resolved": "17.0.51", - "contentHash": "4cMhOmJJl18BU+LTrcjNTLDqWBuCN5t87fB64n7UNyKLs03o4UVNKEjsUwlo6USbccGfoyba4mWPWIo7vL0qkA==", - "dependencies": { - "Microsoft.ServiceHub.Framework": "3.0.2061", - "StreamJsonRpc": "2.8.28" - } - }, - "Microsoft.VisualStudio.Setup.Configuration.Interop": { - "type": "Transitive", - "resolved": "3.0.4492", - "contentHash": "BfkqM96P8+N+cz4T+pxKrIKk2ZD1YMxCXH2ivtBDj5tx6Mc2YQLK1+3h+C6Qebper0RBipuHVn51lb9SZH6bKQ==" - }, - "Microsoft.VisualStudio.Shell.15.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "9bOKENGcO474GIgVoRzmkce6FdW/fiH/W2M0ULyRkFjNh9yEdJheYeyJFBr3Pb0EAwPwu2Q3gayQB7I9oTd/SA==", - "dependencies": { - "Microsoft.VisualStudio.ComponentModelHost": "17.0.487", - "Microsoft.VisualStudio.ImageCatalog": "17.0.31902.203", - "Microsoft.VisualStudio.Imaging": "17.0.31902.203", - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31902.203", - "Microsoft.VisualStudio.Interop": "17.0.31902.203", - "Microsoft.VisualStudio.ProjectAggregator": "17.0.31902.203", - "Microsoft.VisualStudio.Shell.Framework": "17.0.31902.203", - "Microsoft.VisualStudio.Text.Data": "17.0.487", - "Microsoft.VisualStudio.Utilities": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Shell.Design": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "gFVdvJ9HOOnq4ntBCcULraptK+V+Mu1GahUXmd0dhsomGzkzgqDqN7aYQ3ys+K45pJhNeB3MJ/NNQUI4woyiBQ==", - "dependencies": { - "Microsoft.VisualStudio.ImageCatalog": "17.0.31902.203", - "Microsoft.VisualStudio.Interop": "17.0.31902.203", - "Microsoft.VisualStudio.Shell.15.0": "17.0.31902.203", - "Microsoft.VisualStudio.Shell.Framework": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Shell.Framework": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "L9s0/zSMeNlh1Pyzh9vX7T2+O3vg0JxTgXHyXGP/36DpJh4f+87D4rj+/nkESY4YXIUKPVl7XWXh3NLG0yjj1w==", - "dependencies": { - "Microsoft.Build.Framework": "16.5.0", - "Microsoft.VisualStudio.GraphModel": "17.0.31902.203", - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31902.203", - "Microsoft.VisualStudio.Interop": "17.0.31902.203", - "Microsoft.VisualStudio.Telemetry": "16.3.250", - "Microsoft.VisualStudio.Text.Data": "17.0.487", - "Microsoft.VisualStudio.Threading": "17.0.64", - "Microsoft.VisualStudio.Utilities": "17.0.31902.203", - "Newtonsoft.Json": "13.0.1", - "System.ComponentModel.Composition": "4.5.0", - "System.Threading.Tasks.Dataflow": "5.0.0", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "Microsoft.VisualStudio.Shell.Interop": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "GaGSme4K2Wy0b65evinVk7JCl1+Dn7TCxBBwKuzATjWWiY0lvvqLElhXBq+qpC23YN0I5jTBTv0inhZ3G06uMg==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Shell.Interop.10.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "CiHRisP0ts0lbyZ1SR0H7zfiVDTgkQTeORQTDY1xOTy7WL1hIzqmUz/M5UL7d6WcVI4hTAFosJ77NcqYrnTwGg==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Shell.Interop.11.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "doH7HCWzS2bXohKR0Vl1DRjzqV0iw/poZteLS0i4nzPqFs8e2xg+U6a/ysUQ3QHgZ5y7iraefPPmp/VMJfbIeQ==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Shell.Interop.12.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "SfZOz3igcZc7mKOBOr2MrPnliJL0HWF0s9UAL1B05Rc8cI/wBNIbyC1PE8Xt44P6c1vjJm6tWscAFSHZdKpGuA==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Shell.Interop.8.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "rLDc0KVPc1HZEWd5OYyvKGJ2+0eay9gNeh2wMiEJC7UNLHo+JM0wxAYNehze50hWJKlFqefHKJ6Klmp1iX3kpQ==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Shell.Interop.9.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "bz8tlDefGkBIGfF5KSScfs6f7P79IYzX3mqKAlXUshZjYQNd+8hLngls19cCP4oNlOre/DXAhESptNOgVihzzg==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TaskRunnerExplorer.14.0": { - "type": "Transitive", - "resolved": "14.0.0", - "contentHash": "iZpAv8bEWjkyxFF1GIcSOfldqP/umopJKnJGKHa0vg8KR7ZY3u3dWtJmwO4w3abIx+176SIkQe78y5A+/Md7FA==" - }, - "Microsoft.VisualStudio.Telemetry": { - "type": "Transitive", - "resolved": "16.3.250", - "contentHash": "Ijo4HUinCwSdgegeXXzfEYmWuVLC1o9CI3FXW2x4CPKoYemlrN6xcGDUy4oKRxyOsFkjAaiRxR/X9TOgy2xVsQ==", - "dependencies": { - "Microsoft.CSharp": "4.7.0", - "Microsoft.VisualStudio.RemoteControl": "16.3.41", - "Microsoft.VisualStudio.Utilities.Internal": "16.3.23", - "Newtonsoft.Json": "9.0.1" - } - }, - "Microsoft.VisualStudio.Text.Data": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "lrqBhf4lI8Xzk0Z5hwcAtqnYTL1OK+iwu6tHJXDR8Vv7hFvG/YwetOQMJPz6UaLd93PEX+jPh0IfdY/CddoVDQ==", - "dependencies": { - "Microsoft.VisualStudio.CoreUtility": "17.0.487", - "Microsoft.VisualStudio.Threading": "17.0.63" - } - }, - "Microsoft.VisualStudio.Text.Logic": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "ZLMg3fDYijfur0B02/tkB1lmUWQtRfC9xkRr2XlQ/x7MOotTvDQO3HJp9hZyqn9LVBTWbjOnLVcoSmzthsGnnw==", - "dependencies": { - "Microsoft.VisualStudio.CoreUtility": "17.0.487", - "Microsoft.VisualStudio.Text.Data": "17.0.487", - "System.Collections.Immutable": "5.0.0", - "System.ComponentModel.Composition": "4.5.0" - } - }, - "Microsoft.VisualStudio.Text.UI": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "f+7L6k+eyJtQzrr//j2ZtYRDnGJeGSluT10do9/3ajgfT3WpRdlq5HEr60imnHbUU5eY2V/bjbKIu8q6Yg2VfA==", - "dependencies": { - "Microsoft.VisualStudio.CoreUtility": "17.0.487", - "Microsoft.VisualStudio.Text.Data": "17.0.487", - "Microsoft.VisualStudio.Text.Logic": "17.0.487", - "System.ComponentModel.Composition": "4.5.0" - } - }, - "Microsoft.VisualStudio.Text.UI.Wpf": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "Aq+eNxbpBlU8RYsSwh2JM1ZsttVvU7BKgFG6I/356EaY1ZJVz2LhAfjAl6C/A3pmizxgcczQZOIIJsrhcP/OIQ==", - "dependencies": { - "Microsoft.VisualStudio.CoreUtility": "17.0.487", - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31723.112", - "Microsoft.VisualStudio.Text.Data": "17.0.487", - "Microsoft.VisualStudio.Text.Logic": "17.0.487", - "Microsoft.VisualStudio.Text.UI": "17.0.487" - } - }, - "Microsoft.VisualStudio.TextManager.Interop": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "w4+TDaisrknpeXkeebsTQsgl+6gbJivFyZp18/7Yqpo/lyG+IttvE9ZnJ5fFofTyrmgSGLi9BLEifT0mtsEaXA==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TextManager.Interop.10.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "ODXNlirs3ttpe1ooEDsKKw6XbQ06h3kvJua++J0gPqM6vMWS5YfJVH0UxHQ+BDAFZQnUJcbH9b0sVdC9JUKQ6g==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TextManager.Interop.11.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "7NvcHy27r3fmlgdxPrZy4LWAm3yFqNJB3vMTMJv0K/olUzIpNRXYJMeQNltb8AJuOrbpb6HV6L2MIyJDehEADg==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TextManager.Interop.12.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "2ptvBpyBVEtxwUS+FfFKPjbj+l6u/3IyznQegMmEIoQda6oeg07TFL0VAVmsqg48s4FFtpqRklx8kWhMNZqSXQ==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TextManager.Interop.8.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "70mNZyDPIsC+rNov2wHYb6H+V8Woc/FJktj2JsA194eMxk+DPpF0Jx3/sMgkDycV+CjIJog6TguywF4q4milIA==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TextManager.Interop.9.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "s59cD2jJpwEGs6HE0fbp0sE+ZsR4pp8bK91WCelRUVCiMNMtgzb3PVBCjvl082f9UxIsClOBBYDTUjrt/CFqVQ==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TextTemplating": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "IYWBsY2RWM8p2blyT0ja94+YRSc0wmL5er4/DfQShc3JkrQpVmmnWwSjRWC5IEp/dSukMW1LEfStqi/Kc6ZTtQ==", - "dependencies": { - "Microsoft.VisualStudio.TextTemplating.Interfaces": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TextTemplating.Interfaces": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "rCQLkYDAKJ4BGot0CL5dkYhwArR3x2COIiTF5z3cMONVVfQqoXJKXaXve/O22DaTE/28Zt5EnaDNzZHIwROpeg==", - "dependencies": { - "Microsoft.VisualStudio.TextTemplating.Interfaces.11.0": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TextTemplating.Interfaces.10.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "KsY6a4Nk+wOj/0aGAE21JSglQt6+tlsAyPnVyiqqHn+ir1Wx9o0rFtekc6EfSle8nn5RM7QqLGSUR/Bxy6rQbA==" - }, - "Microsoft.VisualStudio.TextTemplating.Interfaces.11.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "pDYRlZTH42mS+r+dIjce6KOt/3HSxX1M1tPldT8VySnQ2PsT/6kRK2UVMOPla9hfJ1dWj2wMpYF8UNN2nuzdjA==", - "dependencies": { - "Microsoft.VisualStudio.TextTemplating.Interfaces.10.0": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TextTemplating.VSHost": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "srtrkd9VCmGFRvMNV6STSe0XqM/DSl7xfY+EUIWhPVO8k+dl+DBNPIZzCK43Enfb+i3OrYFY2yqB6baNaelnaA==", - "dependencies": { - "Microsoft.VisualStudio.Shell.Framework": "17.0.31902.203", - "Microsoft.VisualStudio.TextTemplating": "17.0.31902.203", - "Microsoft.VisualStudio.Validation": "17.0.28", - "System.Runtime.CompilerServices.Unsafe": "5.0.0" - } - }, - "Microsoft.VisualStudio.Threading": { - "type": "Transitive", - "resolved": "17.0.64", - "contentHash": "HD/yoC7u1Ignj/EsCST4iFXl8zaE+8r2A+4CUkl6GLTJjdNjfl8iNvhqpyK8+DjCMwhyNRRH0I6S6FA37fz95Q==", - "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "5.0.0", - "Microsoft.VisualStudio.Threading.Analyzers": "17.0.64", - "Microsoft.VisualStudio.Validation": "16.10.35", - "Microsoft.Win32.Registry": "5.0.0", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "Microsoft.VisualStudio.Threading.Analyzers": { - "type": "Transitive", - "resolved": "17.0.64", - "contentHash": "+xz3lAqA3h2/5q6H7Udmz9TsxDQ99O+PjoQ4k4BTO3SfAfyJX7ejh7I1D1N/M/GzGUci9YOUpr6KBO4vXLg+zQ==" - }, - "Microsoft.VisualStudio.Utilities": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "j0c5dq1ZmFFAi4kB4wJ1BOUrb1kJyhFvaqDKymbdJNaib8DdG8PNAdLrW1l0RewsEMDy5Rh2cO7mzrG/FlyE5Q==", - "dependencies": { - "Microsoft.ServiceHub.Client": "3.0.3078", - "Microsoft.VisualStudio.RpcContracts": "17.0.51", - "Microsoft.VisualStudio.Telemetry": "16.3.250", - "System.ComponentModel.Composition": "4.5.0", - "System.Threading.AccessControl": "5.0.0", - "System.Threading.Tasks.Dataflow": "5.0.0" - } - }, - "Microsoft.VisualStudio.Utilities.Internal": { - "type": "Transitive", - "resolved": "16.3.23", - "contentHash": "AxbS8vXJj0IjTv67JbmOqwJERYUDE7BHbXYkXGiyqYblizMjhVdohNIethnJX9lVN2RmotN5GQbwLWDoMKatvw==" - }, - "Microsoft.VisualStudio.Validation": { - "type": "Transitive", - "resolved": "17.0.28", - "contentHash": "qT+0Qv7lxLt7NKQjkroi34s8cDXVPWA3vDkvoFZwM9PRmZ28aKrMLaQRnkT7rgBYLf+mNtr2najktKUzkAtP6Q==" - }, - "Microsoft.VisualStudio.VCProjectEngine": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "QjjZguY7FypxVasdPihUbVSP5PjwBPiitCgnC/ZPePFO5KYzJ99VxdlASDDxNGVq5+q/+3YlBD94PXComDRu2w==" - }, - "Microsoft.VisualStudio.VSHelp": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "PT4aoOrZfPHIpTq3lwamypIrx0hR7d5iLlHewwhjUNezQ8ElN/jbWjvx74r/uf2g6dVPiQcS4HI0RGgAb4MQ2A==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.VSHelp80": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "B/G69KkAcMIwXgEvGAMpGSq1PvMx9JKAVzQdKiX4gm2DMFEsT2Id89TxS0oPg2X7QC3JBCk+/+BDUjtWPkjqMA==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.WCFReference.Interop": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "45HixSYn5IBSgc6MmVHhivWCiRgdZ8BruXOC9fUrQIRrYHu8DR9aw8imnFImmJY3Mii92VHV4NKPYOgUkXNd/Q==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Web.BrowserLink.12.0": { - "type": "Transitive", - "resolved": "12.0.0", - "contentHash": "HeuaZh8+wNVdwx7VF8guFGH2Z2zH+FYxWBsRNp+FjjlmrhCfM7GUQV5azaTv/bN5TPaK8ALJoP9UX5o1FB5k1A==" - }, - "Microsoft.Win32.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==" - }, - "Microsoft.Win32.Registry": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", - "dependencies": { - "System.Security.AccessControl": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, - "Nerdbank.Streams": { - "type": "Transitive", - "resolved": "2.6.81", - "contentHash": "htBHFE359qyyFwrvAGvFxrbBAoldZdl0XjtQdDWTJ8t5sWWs7QVXID5y1ZGJE61UgpV5CqWsj/NT0LOAn5GdZw==", - "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "1.1.1", - "Microsoft.VisualStudio.Threading": "16.7.56", - "Microsoft.VisualStudio.Validation": "15.5.31", - "System.IO.Pipelines": "4.7.2", - "System.Net.WebSockets": "4.3.0", - "System.Runtime.CompilerServices.Unsafe": "4.7.1" - } - }, - "stdole": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "c1WK5n8poXt1fafyuS+ntL5w0tc3P/r4F7+aeUaLYTp2/YoQB8XUpQFWqytZdtu7EAuMXoZ14T7jh7Vl+M4ZfQ==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "StreamJsonRpc": { - "type": "Transitive", - "resolved": "2.8.28", - "contentHash": "i2hKUXJSLEoWpPqQNyISqLDqmFHMiyasjTC/PrrHNWhQyauFeVoebSct3E4OTUzRC1DYjVJ9AMiVbp/uVYLnjQ==", - "dependencies": { - "MessagePack": "2.2.85", - "Microsoft.Bcl.AsyncInterfaces": "5.0.0", - "Microsoft.VisualStudio.Threading": "16.9.60", - "Nerdbank.Streams": "2.6.81", - "Newtonsoft.Json": "12.0.2", - "System.Collections.Immutable": "5.0.0", - "System.Diagnostics.DiagnosticSource": "5.0.1", - "System.IO.Pipelines": "5.0.1", - "System.Memory": "4.5.4", - "System.Net.Http": "4.3.4", - "System.Net.WebSockets": "4.3.0", - "System.Reflection.Emit": "4.7.0", - "System.Threading.Tasks.Dataflow": "5.0.0", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "StrongNamer": { - "type": "Transitive", - "resolved": "0.0.8", - "contentHash": "7GAMIhQSKabPrXVtEpDQoLRWVZUn1IGzeHvfDSvs5lPhU4KPFMQVpI/lDFikhMXr8FmdFBOBGaHEhQ9azMK3TA==" - }, - "System.Buffers": { - "type": "Transitive", - "resolved": "4.5.1", - "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" - }, - "System.Collections.Immutable": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "FXkLXiK0sVVewcso0imKQoOxjoPAj42R8HtjjbSjVPAzwDfzoyoznWxgA3c38LDbN9SJux1xXoXYAhz98j7r2g==", - "dependencies": { - "System.Memory": "4.5.4" - } - }, - "System.ComponentModel.Composition": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "+iB9FoZnfdqMEGq6np28X6YNSUrse16CakmIhV3h6PxEWt7jYxUN3Txs1D8MZhhf4QmyvK0F/EcIN0f4gGN0dA==" - }, - "System.Composition": { - "type": "Transitive", - "resolved": "1.0.31", - "contentHash": "I+D26qpYdoklyAVUdqwUBrEIckMNjAYnuPJy/h9dsQItpQwVREkDFs4b4tkBza0kT2Yk48Lcfsv2QQ9hWsh9Iw==", - "dependencies": { - "System.Composition.AttributedModel": "1.0.31", - "System.Composition.Convention": "1.0.31", - "System.Composition.Hosting": "1.0.31", - "System.Composition.Runtime": "1.0.31", - "System.Composition.TypedParts": "1.0.31" - } - }, - "System.Composition.Convention": { - "type": "Transitive", - "resolved": "1.0.31", - "contentHash": "GLjh2Ju71k6C0qxMMtl4efHa68NmWeIUYh4fkUI8xbjQrEBvFmRwMDFcylT8/PR9SQbeeL48IkFxU/+gd0nYEQ==", - "dependencies": { - "System.Composition.AttributedModel": "1.0.31" - } - }, - "System.Composition.Hosting": { - "type": "Transitive", - "resolved": "1.0.31", - "contentHash": "fN1bT4RX4vUqjbgoyuJFVUizAl2mYF5VAb+bVIxIYZSSc0BdnX+yGAxcavxJuDDCQ1K+/mdpgyEFc8e9ikjvrg==", - "dependencies": { - "System.Composition.Runtime": "1.0.31" - } - }, - "System.Composition.Runtime": { - "type": "Transitive", - "resolved": "1.0.31", - "contentHash": "0LEJN+2NVM89CE4SekDrrk5tHV5LeATltkp+9WNYrR+Huiyt0vaCqHbbHtVAjPyeLWIc8dOz/3kthRBj32wGQg==" - }, - "System.Composition.TypedParts": { - "type": "Transitive", - "resolved": "1.0.31", - "contentHash": "0Zae/FtzeFgDBBuILeIbC/T9HMYbW4olAmi8XqqAGosSOWvXfiQLfARZEhiGd0LVXaYgXr0NhxiU1LldRP1fpQ==", - "dependencies": { - "System.Composition.AttributedModel": "1.0.31", - "System.Composition.Hosting": "1.0.31", - "System.Composition.Runtime": "1.0.31" - } - }, - "System.Diagnostics.DiagnosticSource": { - "type": "Transitive", - "resolved": "5.0.1", - "contentHash": "uXQEYqav2V3zP6OwkOKtLv+qIi6z3m1hsGyKwXX7ZA7htT4shoVccGxnJ9kVRFPNAsi1ArZTq2oh7WOto6GbkQ==", - "dependencies": { - "System.Memory": "4.5.4", - "System.Runtime.CompilerServices.Unsafe": "5.0.0" - } - }, - "System.IO": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==" - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "5.0.1", - "contentHash": "qEePWsaq9LoEEIqhbGe6D5J8c9IqQOUuTzzV6wn1POlfdLkJliZY3OlB0j0f17uMWlqZYjH7txj+2YbyrIA8Yg==", - "dependencies": { - "System.Buffers": "4.5.1", - "System.Memory": "4.5.4", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "System.Memory": { - "type": "Transitive", - "resolved": "4.5.4", - "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==", - "dependencies": { - "System.Buffers": "4.5.1", - "System.Numerics.Vectors": "4.5.0", - "System.Runtime.CompilerServices.Unsafe": "4.5.3" - } - }, - "System.Net.Http": { - "type": "Transitive", - "resolved": "4.3.4", - "contentHash": "aOa2d51SEbmM+H+Csw7yJOuNZoHkrP2XnAurye5HWYgGVVU54YZDvsLUYRv6h18X3sPnjNCANmN7ZhIPiqMcjA==", - "dependencies": { - "System.Security.Cryptography.X509Certificates": "4.3.0" - } - }, - "System.Net.WebSockets": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "u6fFNY5q4T8KerUAVbya7bR6b7muBuSTAersyrihkcmE5QhEOiH3t5rh4il15SexbVlpXFHGuMwr/m8fDrnkQg==" - }, - "System.Numerics.Vectors": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" - }, - "System.Private.Uri": { - "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "o1+7RJnu3Ik3PazR7Z7tJhjPdE000Eq2KGLLWhqJJKXj04wrS8lwb1OFtDF9jzXXADhUuZNJZlPc98uwwqmpFA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.1", - "Microsoft.NETCore.Targets": "1.1.3" - } - }, - "System.Reflection.Emit": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "VR4kk8XLKebQ4MZuKuIni/7oh+QGFmZW3qORd1GvBq/8026OpW501SzT/oypwiQl4TvT8ErnReh/NzY9u+C6wQ==" - }, - "System.Reflection.Emit.Lightweight": { - "type": "Transitive", - "resolved": "4.6.0", - "contentHash": "j/V5HVvxvBQ7uubYD0PptQW2KGsi1Pc2kZ9yfwLixv3ADdjL/4M78KyC5e+ymW612DY8ZE4PFoZmWpoNmN2mqg==" - }, - "System.Reflection.Metadata": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "5NecZgXktdGg34rh1OenY1rFNDCI8xSjFr+Z4OU4cU06AQHUdRnIIEeWENu3Wl4YowbzkymAIMvi3WyK9U53pQ==", - "dependencies": { - "System.Collections.Immutable": "5.0.0" - } - }, - "System.Runtime": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==" - }, - "System.Runtime.CompilerServices.Unsafe": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "ZD9TMpsmYJLrxbbmdvhwt9YEgG5WntEnZ/d1eH8JBX9LBp+Ju8BSBhUGbZMNVHHomWo2KVImJhTDl2hIgw/6MA==" - }, - "System.Security.AccessControl": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", - "dependencies": { - "System.Security.Principal.Windows": "5.0.0" - } - }, - "System.Security.Cryptography.Algorithms": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", - "dependencies": { - "System.IO": "4.3.0", - "System.Runtime": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0" - } - }, - "System.Security.Cryptography.Encoding": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==" - }, - "System.Security.Cryptography.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==" - }, - "System.Security.Cryptography.X509Certificates": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", - "dependencies": { - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0" - } - }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" - }, - "System.Text.Encoding.CodePages": { - "type": "Transitive", - "resolved": "4.5.1", - "contentHash": "4J2JQXbftjPMppIHJ7IC+VXQ9XfEagN92vZZNoG12i+zReYlim5dMoXFC1Zzg7tsnKDM7JPo5bYfFK4Jheq44w==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "4.5.2" - } - }, - "System.Threading.AccessControl": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "WJ9w9m4iHJVq0VoH7hZvYAccbRq95itYRhAAXd6M4kVCzLmT6NqTwmSXKwp3oQilWHhYTXgqaIXxBfg8YaqtmA==", - "dependencies": { - "System.Security.AccessControl": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, - "System.Threading.Channels": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==", - "dependencies": { - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "System.Threading.Tasks.Dataflow": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "NBp0zSAMZp4muDje6XmbDfmkqw9+qsDCHp+YMEtnVgHEjQZ3Q7MzFTTp3eHqpExn4BwMrS7JkUVOTcVchig4Sw==" - }, - "System.Threading.Tasks.Extensions": { - "type": "Transitive", - "resolved": "4.5.4", - "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "4.5.3" - } - }, - "VSLangProj": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "XjyhxIyXjPitKrxeWkF1yaiqT02Klct5yZHz93x9O4nLTEnbmAHb+HRNiIIgNMXvG2sboNIvTt1MpuR1AeJktw==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj100": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "8pwHJP2XefsWxhL3dQ11MBD9WwxP3SjAO41t6ZcFSfTHtSQFsp7erdtBrKzuFXpnIp3WoVc26f7jWrt4rr66+g==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj110": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "UToccqkFdocw9LZx7Rn62/puqxPjF7MQWIk2yMGqIdHLcaeRmR6gI1/3Ea+57ZcVehhuEvtxH8jXV2t4HwwU8g==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj140": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "ssRCHLYW5nSJJBy3rad5JId8SjcXHOsOzVW4ohZgtnHBtf73ehIJLtr+oRc7JK3NasmWBuB9DJqwwJBLj4wn3Q==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj150": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "sxFOk5x5bDqyxUgfi5zF58E8BnfRQ17Ft0IbxJDS0Bvf+6CLYjWQzKXqxCczYHs2aD7upUpN1hrf93bHlqahAQ==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj157": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "jYp9X00aaJKttYL1IuME8mpvd349H2zDbaFIBRf6LnDUPHd7i7MsnaREpID1Dr+H37P16pzbv48taLKUnAoBsw==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj158": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "bwHHmgclOjBNI/CpUqgPH274D7UKDrgzFAEtFULixlOhA6E4NfYOgdUdemiW5bg58i4ao60T9tz3ac6PQfHeAw==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj165": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "Brh37fZOyPL0zC9XVtOzWRl22GZZnN6/TVEvg6PDhWhjgnkdHv8hz0GZCIzilbyl9ilOJnQCx8oVQ+KMfcvXFQ==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj2": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "hqP1IODtJneTrUwwXeqD0YHmqyde3/KJaJRJehsn07TQxYxzyigrpNsh8GEoExTSis0SoFt97tXTztbXiBvSvg==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj80": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "qm0T7dPige52L6mnsk+3Q8QV8R/Rgdkdpb3FaUMolP4JWU4s2OfrUzFajFBf8auf/NBQgUcBvE1Kr5yRpYmL2Q==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj90": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "KoyI+jvJ9fHwR2uGGlUxOqAmTgDO1naxwXbt504y/Jx1h/BXnhCWipGdyM/lS83GUGsmx8RhZbiiMlZw7LmYhw==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "SonarLint.VisualStudio.ConnectedMode": { - "type": "Project", - "dependencies": { - "Microsoft.Alm.Authentication": "[4.0.0.1, )", - "Microsoft.VisualStudio.Sdk": "[17.0.31902.203, )", - "Newtonsoft.Json": "[13.0.3, )", - "SonarLint.VisualStudio.Core": "[1.0.0, )", - "SonarLint.VisualStudio.IssueVisualization": "[1.0.0, )", - "SonarLint.VisualStudio.SLCore": "[1.0.0, )", - "StrongNamer": "[0.0.8, )" - } - }, - "SonarLint.VisualStudio.Core": { - "type": "Project", - "dependencies": { - "BouncyCastle.Cryptography": "[2.4.0, )", - "Newtonsoft.Json": "[13.0.3, )", - "System.Collections.Immutable": "[5.0.0, )", - "System.IO.Abstractions": "[9.0.4, )", - "System.Threading.Channels": "[7.0.0, )" - } - }, - "SonarLint.VisualStudio.Infrastructure.VS": { - "type": "Project", - "dependencies": { - "Microsoft.VisualStudio.Sdk": "[17.0.31902.203, )", - "SonarLint.VisualStudio.Core": "[1.0.0, )" - } - }, - "SonarLint.VisualStudio.IssueVisualization": { - "type": "Project", - "dependencies": { - "Microsoft.VisualStudio.Sdk": "[17.0.31902.203, )", - "SonarLint.VisualStudio.Core": "[1.0.0, )", - "SonarLint.VisualStudio.Infrastructure.VS": "[1.0.0, )", - "SonarLint.VisualStudio.SLCore": "[1.0.0, )", - "SonarQube.Client": "[1.0.0, )" - } - }, - "SonarLint.VisualStudio.SLCore": { - "type": "Project", - "dependencies": { - "SonarLint.VisualStudio.Core": "[1.0.0, )", - "StreamJsonRpc": "[2.6.121, )" - } - }, - "sonarqube.client": { - "type": "Project", - "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", - "Newtonsoft.Json": "[13.0.3, )", - "SonarLint.VisualStudio.Core": "[1.0.0, )", - "System.Net.Http": "[4.0.0, )" - } - } - } - } -} \ No newline at end of file diff --git a/src/RoslynAnalyzerServer.IntegrationTests/GlobalUsings.cs b/src/RoslynAnalyzerServer.IntegrationTests/GlobalUsings.cs new file mode 100644 index 0000000000..8e8fa28ddd --- /dev/null +++ b/src/RoslynAnalyzerServer.IntegrationTests/GlobalUsings.cs @@ -0,0 +1,25 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +global using System; +global using FluentAssertions; +global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using Moq; +global using NSubstitute; diff --git a/src/RoslynAnalyzerServer.IntegrationTests/Http/Helper/HttpRequester.cs b/src/RoslynAnalyzerServer.IntegrationTests/Http/Helper/HttpRequester.cs new file mode 100644 index 0000000000..d928fba3b2 --- /dev/null +++ b/src/RoslynAnalyzerServer.IntegrationTests/Http/Helper/HttpRequester.cs @@ -0,0 +1,44 @@ +using System.Net.Http; +using System.Security; +using System.Text; +using Newtonsoft.Json; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; +using SonarLint.VisualStudio.SLCore.Common.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.IntegrationTests.Http.Helper; + +internal record AnalysisRequestConfig(SecureString Token, string RequestUri, T Request); + +internal sealed class HttpRequester : IDisposable +{ + private const int WaitForServerMsTimeout = 2000; + private const string JsonMediaType = "application/json"; + private const string XAuthTokenHeader = "X-Auth-Token"; + private readonly HttpClient httpClient; + + public HttpRequester(int requestTimeout = WaitForServerMsTimeout) + { + httpClient = new HttpClient(); + httpClient.Timeout = TimeSpan.FromMilliseconds(requestTimeout); + } + + public void Dispose() => httpClient.Dispose(); + + internal async Task SendRequest(AnalysisRequestConfig analysisRequestConfig) + { + var body = JsonConvert.SerializeObject(analysisRequestConfig.Request); + + return await SendRequest(analysisRequestConfig.Token.ToUnsecureString(), analysisRequestConfig.RequestUri, body); + } + + internal async Task SendRequest(string token, string requestUri, string body) + { + var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Headers.Add(XAuthTokenHeader, token); + request.Content = new StringContent(body, Encoding.UTF8, JsonMediaType); + + var response = await httpClient.SendAsync(request); + return response; + } +} diff --git a/src/RoslynAnalyzerServer.IntegrationTests/Http/Helper/HttpServerStarter.cs b/src/RoslynAnalyzerServer.IntegrationTests/Http/Helper/HttpServerStarter.cs new file mode 100644 index 0000000000..24f825ed13 --- /dev/null +++ b/src/RoslynAnalyzerServer.IntegrationTests/Http/Helper/HttpServerStarter.cs @@ -0,0 +1,108 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.IntegrationTests.Http.Helper; + +internal sealed class HttpServerStarter : IDisposable +{ + private const int WaitForServerMsTimeout = 1000; + internal readonly IHttpServerSettings ServerSettings; + internal IHttpServerConfigurationProvider HttpServerConfigurationProvider { get; } + internal RoslynAnalysisHttpServer RoslynAnalysisHttpServer { get; } + internal ILogger MockedLogger { get; } = CreateMockedLogger(); + internal IRoslynAnalysisService MockedRoslynAnalysisService { get; } = CreateMockedAnalysisEngine(); + + public HttpServerStarter(bool useMockedServerSettings = false, int maxConcurrentRequests = 5, bool useMockedServerConfiguration = false) + { + ServerSettings = useMockedServerSettings ? CreateMockedServerConfiguration(maxConcurrentRequests) : new HttpServerSettings(); + var httpServerConfigurationProvider = new HttpServerConfigurationProvider(); + HttpServerConfigurationProvider = useMockedServerConfiguration ? CreateMockedServerConfigurationProvider() : httpServerConfigurationProvider; + var httpServerConfigurationFactory = useMockedServerConfiguration ? CreateHttpServerConfigurationFactory() : httpServerConfigurationProvider; + var analysisRequestHandler = new AnalysisRequestHandler(MockedLogger, ServerSettings, HttpServerConfigurationProvider); + RoslynAnalysisHttpServer = new RoslynAnalysisHttpServer(MockedLogger, ServerSettings, analysisRequestHandler, new HttpRequestHandler(), + new HttpListenerFactory(), httpServerConfigurationFactory, MockedRoslynAnalysisService); + } + + public void StartListeningOnBackgroundThread() + { + var serverListeningEvent = new ManualResetEvent(false); + MockedLogger.When(x => x.LogVerbose(Arg.Is(x => x == Resources.HttpServerStarted || x == Resources.HttpServerNotStarted))).Do(_ => + { + serverListeningEvent.Set(); + }); + MockedLogger.When(x => x.LogVerbose(Resources.HttpServerFailedToStartAttempts, ServerSettings.MaxStartAttempts)).Do(_ => + { + serverListeningEvent.Set(); + }); + var thread = new Thread(() => StartRoslynAnalysisHttpServer(RoslynAnalysisHttpServer).ConfigureAwait(false)) { IsBackground = true }; + thread.Start(); + serverListeningEvent.WaitOne(WaitForServerMsTimeout); + } + + public void Dispose() => RoslynAnalysisHttpServer.Dispose(); + + private static async Task StartRoslynAnalysisHttpServer(RoslynAnalysisHttpServer httpServer) => await httpServer.StartListenAsync(); + + private static ILogger CreateMockedLogger() + { + var logger = Substitute.For(); + logger.ForContext(Arg.Any()).Returns(logger); + return logger; + } + + private static IRoslynAnalysisService CreateMockedAnalysisEngine() + { + var analysisEngine = Substitute.For(); + analysisEngine.AnalyzeAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(Enumerable.Empty())); + return analysisEngine; + } + + private static IHttpServerSettings CreateMockedServerConfiguration(int maxConcurrentRequests) + { + var config = Substitute.For(); + config.MaxConcurrentRequests.Returns(maxConcurrentRequests); + config.MaxStartAttempts.Returns(3); + config.RequestMillisecondsTimeout.Returns(3000); + config.MaxRequestBodyBytes.Returns(1024); + return config; + } + + private static IHttpServerConfigurationProvider CreateMockedServerConfigurationProvider() + { + var config = Substitute.For(); + config.CurrentConfiguration.Returns(Substitute.For()); + return config; + } + + private IHttpServerConfigurationFactory CreateHttpServerConfigurationFactory() + { + var config = Substitute.For(); + var configuration = Substitute.For(); + config.SetNewConfiguration().Returns(configuration); + HttpServerConfigurationProvider.CurrentConfiguration.Returns(configuration); + + return config; + } +} diff --git a/src/RoslynAnalyzerServer.IntegrationTests/Http/RoslynAnalysisHttpServerTest.cs b/src/RoslynAnalyzerServer.IntegrationTests/Http/RoslynAnalysisHttpServerTest.cs new file mode 100644 index 0000000000..f51c3e6049 --- /dev/null +++ b/src/RoslynAnalyzerServer.IntegrationTests/Http/RoslynAnalysisHttpServerTest.cs @@ -0,0 +1,345 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Net; +using System.Net.Http; +using System.Security; +using System.Text; +using Newtonsoft.Json; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; +using SonarLint.VisualStudio.RoslynAnalyzerServer.IntegrationTests.Http.Helper; +using SonarLint.VisualStudio.SLCore.Common.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.IntegrationTests.Http; + +[TestClass] +public class RoslynAnalysisHttpServerTest +{ + private static readonly HttpServerStarter ServerStarter = new(); + private static readonly HttpRequester HttpRequester = new(); + private const string CsharpFileName = "C:\\MyFile.cs"; + + [ClassInitialize] + public static void ClassInitialize(TestContext context) => ServerStarter.StartListeningOnBackgroundThread(); + + [ClassCleanup] + public static void ClassCleanup() + { + ServerStarter.Dispose(); + HttpRequester.Dispose(); + } + + [TestMethod] + public async Task StartListenAsync_CallsMultipleTimes_DoesNotStartIfAlreadyStarted() + { + var port = ServerStarter.HttpServerConfigurationProvider.CurrentConfiguration.Port; + await VerifyServerReachable(CreateClientRequestConfig()); + + await ServerStarter.RoslynAnalysisHttpServer.StartListenAsync(); + await ServerStarter.RoslynAnalysisHttpServer.StartListenAsync(); + + ServerStarter.HttpServerConfigurationProvider.CurrentConfiguration.Port.Should().Be(port); + ServerStarter.MockedLogger.Received(1).LogVerbose(Resources.HttpServerStarted); + await VerifyServerReachable(CreateClientRequestConfig()); + } + + [TestMethod] + public async Task StartListenAsync_StartsAfterDisposed_DoesNotStart() + { + using var serverStarter = new HttpServerStarter(); + serverStarter.StartListeningOnBackgroundThread(); + + serverStarter.RoslynAnalysisHttpServer.Dispose(); + await serverStarter.RoslynAnalysisHttpServer.StartListenAsync(); + + await VerifyServerNotReachable(CreateClientRequestConfig(serverStarter)); // the timeout of the request should be reached + serverStarter.MockedLogger.Received(1).LogVerbose(Resources.HttpServerStarted); + serverStarter.MockedLogger.Received(1).LogVerbose(Resources.HttpServerDisposed); + } + + [TestMethod] + public async Task StartListenAsync_PortAlreadyInUse_TriesAgain() + { + using var serverStarter2 = new HttpServerStarter(useMockedServerConfiguration: true); + var busyPort = ServerStarter.HttpServerConfigurationProvider.CurrentConfiguration.Port; + MockServerConfiguration(serverStarter2.HttpServerConfigurationProvider, busyPort); + + serverStarter2.StartListeningOnBackgroundThread(); + + serverStarter2.MockedLogger.Received(1).LogVerbose(Resources.HttpServerAttemptFailed, 1, busyPort, Arg.Any()); + await VerifyServerReachable(CreateClientRequestConfig(serverStarter2)); + } + + [TestMethod] + public void StartListenAsync_PortAlreadyInUse_TriesMaxAttempts() + { + using var serverStarter2 = new HttpServerStarter(useMockedServerConfiguration: true); + var busyPort = ServerStarter.HttpServerConfigurationProvider.CurrentConfiguration.Port; + MockServerConfiguration(serverStarter2.HttpServerConfigurationProvider, busyPort); + var maxAttempts = serverStarter2.ServerSettings.MaxStartAttempts; + + serverStarter2.StartListeningOnBackgroundThread(); + + serverStarter2.MockedLogger.Received(maxAttempts) + .LogVerbose(Resources.HttpServerAttemptFailed, Arg.Is(x => x >= 1 && x <= maxAttempts), busyPort, Arg.Is(x => x.Contains("Failed to listen on prefix"))); + serverStarter2.MockedLogger.Received(1).LogVerbose(Resources.HttpServerFailedToStartAttempts, maxAttempts); + } + + [TestMethod] + public async Task StartListenAsync_AnalysisRequestTakesLongerThanTimeout_ClosesRequestAfterTimeout() + { + var millisecondTimeout = 5; + using var serverStarter2 = new HttpServerStarter(useMockedServerSettings: true); + MockServerSettings(serverStarter2.ServerSettings, requestTimeout: millisecondTimeout); + SimulateAnalysisRunsOutOfTime(serverStarter2.MockedRoslynAnalysisService); + serverStarter2.StartListeningOnBackgroundThread(); + + var response = await HttpRequester.SendRequest(CreateClientRequestConfig(serverStarter2)); + + response.StatusCode.Should().Be(HttpStatusCode.RequestTimeout); + serverStarter2.MockedLogger.Received(1).LogVerbose(Arg.Any(), Resources.HttpRequestTimedOut, Arg.Any()); + } + + [DataTestMethod] + [DataRow(true)] + [DataRow(false)] + public async Task Cancel_ReturnsExpectedResult(bool isCanceled) + { + var analysisId = Guid.NewGuid(); + ServerStarter.MockedRoslynAnalysisService.Cancel(Arg.Is(x => x.AnalysisId == analysisId)).Returns(isCanceled); + ServerStarter.StartListeningOnBackgroundThread(); + + var response = await HttpRequester.SendRequest(CreateCancellationRequestConfig(ServerStarter, analysisId)); + + response.StatusCode.Should().Be(isCanceled ? HttpStatusCode.OK : HttpStatusCode.NotFound); + } + + [TestMethod] + public async Task UnknownUrl_ReturnsBadRequest() + { + var unknownUrlRequest = new AnalysisRequestConfig(ServerStarter.HttpServerConfigurationProvider.CurrentConfiguration.Token, + GetRequestUrl(ServerStarter.HttpServerConfigurationProvider.CurrentConfiguration.Port, requestPath: "UNKNOWNURL"), + new { }); + ServerStarter.StartListeningOnBackgroundThread(); + + var response = await HttpRequester.SendRequest(unknownUrlRequest); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + [TestMethod] + public async Task StartListenAsync_ExceededBodyLength_Fails() + { + var bodyLength = 1; + var largeBody = "{}"; + using var serverStarter2 = new HttpServerStarter(useMockedServerSettings: true); + MockServerSettings(serverStarter2.ServerSettings, maxBodyLength: bodyLength); + serverStarter2.StartListeningOnBackgroundThread(); + + var response = await HttpRequester.SendRequest(serverStarter2.HttpServerConfigurationProvider.CurrentConfiguration.Token.ToUnsecureString(), + GetRequestUrl(serverStarter2.HttpServerConfigurationProvider.CurrentConfiguration.Port), + largeBody); + + serverStarter2.MockedLogger.Received(1).LogVerbose(Resources.BodyLengthExceeded, (long)Encoding.UTF8.GetByteCount(largeBody), (long)bodyLength); + response.StatusCode.Should().Be(HttpStatusCode.RequestEntityTooLarge); + } + + [TestMethod] + public async Task StartListenAsync_ProvidesWrongBody_Fails() + { + var unexpectedBodyContent = @" +{ + ""$type"": ""System.Windows.Data.ObjectDataProvider, PresentationFramework"", + ""MethodName"": ""Start"", + ""ObjectInstance"": { + ""$type"": ""System.Diagnostics.Process, System"", + ""StartInfo"": { + ""$type"": ""System.Diagnostics.ProcessStartInfo, System"", + ""FileName"": ""malicious.exe"" + } + } +}"; + + var response = await HttpRequester.SendRequest(ServerStarter.HttpServerConfigurationProvider.CurrentConfiguration.Token.ToUnsecureString(), + GetRequestUrl(ServerStarter.HttpServerConfigurationProvider.CurrentConfiguration.Port), unexpectedBodyContent); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + [TestMethod] + public async Task StartListenAsync_ValidRequest_ReturnsEmptyDiagnostics() + { + var response = await HttpRequester.SendRequest(CreateClientRequestConfig()); + + await VerifyRequestSucceeded(response); + } + + [TestMethod] + public async Task StartListenAsync_InvalidToken_ReturnsUnauthorized() + { + var response = await HttpRequester.SendRequest(CreateClientRequestConfig(token: "wrongToken".ToSecureString())); + + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + + [TestMethod] + public async Task StartListenAsync_InvalidRequestUri_ReturnsNotFound() + { + var invalidRequestUrl = GetRequestUrl(ServerStarter.HttpServerConfigurationProvider.CurrentConfiguration.Port, "wrongPath"); + + var response = await HttpRequester.SendRequest(CreateClientRequestConfig(requestUri: invalidRequestUrl)); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + [TestMethod] + public async Task StartListenAsync_NoFilesToAnalyze_ReturnsBadRequest() + { + var response = await HttpRequester.SendRequest(CreateClientRequestConfig(fileNames: [])); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + [TestMethod] + public async Task StartListenAsync_AnalysisThrowsException_ReturnsInternalServerError() + { + var exceptionMessage = "Simulated exception"; + using var serverStarter2 = new HttpServerStarter(); + serverStarter2.MockedRoslynAnalysisService + .When(x => x.AnalyzeAsync(Arg.Any(), Arg.Any())) + .Do(_ => throw new InvalidOperationException(exceptionMessage)); + serverStarter2.StartListeningOnBackgroundThread(); + + var response = await HttpRequester.SendRequest(CreateClientRequestConfig(serverStarter2)); + + response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + serverStarter2.MockedLogger.Received(1).LogVerbose(Arg.Any(), Resources.HttpRequestFailed, Arg.Is(x => x.Contains(exceptionMessage))); + } + + [TestMethod] + public async Task Dispose_StopsServer() + { + var testServerStarter = new HttpServerStarter(); + testServerStarter.StartListeningOnBackgroundThread(); + + testServerStarter.RoslynAnalysisHttpServer.Dispose(); + + await VerifyServerNotReachable(CreateClientRequestConfig(testServerStarter)); // the timeout of the request should be reached + testServerStarter.MockedLogger.Received(1).LogVerbose(Resources.HttpServerDisposed); + } + + [TestMethod] + public void Dispose_CallsMultipleTimes_DisposesOnce() + { + var testServerStarter = new HttpServerStarter(); + testServerStarter.StartListeningOnBackgroundThread(); + + testServerStarter.RoslynAnalysisHttpServer.Dispose(); + testServerStarter.RoslynAnalysisHttpServer.Dispose(); + testServerStarter.RoslynAnalysisHttpServer.Dispose(); + + testServerStarter.MockedLogger.Received(1).LogVerbose(Resources.HttpServerDisposed); + } + + private static AnalysisRequestConfig CreateClientRequestConfig(HttpServerStarter httpServerStarter, Guid? analysisId = null) => + CreateClientRequestConfig( + [CsharpFileName], + GetRequestUrl(httpServerStarter.HttpServerConfigurationProvider.CurrentConfiguration.Port), + httpServerStarter.HttpServerConfigurationProvider.CurrentConfiguration.Token, + analysisId); + + private static AnalysisRequestConfig CreateCancellationRequestConfig(HttpServerStarter httpServerStarter, Guid analysisId) => + new(httpServerStarter.HttpServerConfigurationProvider.CurrentConfiguration.Token, + GetRequestUrl(httpServerStarter.HttpServerConfigurationProvider.CurrentConfiguration.Port, requestPath: "cancel"), + new AnalysisCancellationRequest { AnalysisId = analysisId }); + + private static AnalysisRequestConfig CreateClientRequestConfig(SecureString? token = null, string? requestUri = null) => + CreateClientRequestConfig([CsharpFileName], requestUri, token); + + private static AnalysisRequestConfig CreateClientRequestConfig( + string[] fileNames, + string? requestUri = null, + SecureString? token = null, + Guid? analysisId = null) + { + token ??= ServerStarter.HttpServerConfigurationProvider.CurrentConfiguration.Token; + requestUri ??= GetRequestUrl(ServerStarter.HttpServerConfigurationProvider.CurrentConfiguration.Port); + var fileUris = fileNames.Select(x => new FileUri(x)); + var analysisRequest = new AnalysisRequest { FileUris = [.. fileUris], ActiveRules = [new ActiveRuleDto("id", [])], AnalysisId = analysisId ?? Guid.NewGuid() }; + return new AnalysisRequestConfig(token, requestUri, analysisRequest); + } + + private static string GetRequestUrl(int port, string requestPath = "analyze") => $"http://127.0.0.1:{port}/{requestPath}"; + + private static async Task VerifyServerReachable(AnalysisRequestConfig requestConfig) + { + var response = await HttpRequester.SendRequest(requestConfig); + await VerifyRequestSucceeded(response); + } + + private static async Task VerifyRequestSucceeded(HttpResponseMessage response) + { + response.StatusCode.Should().Be(HttpStatusCode.OK); + var analysisResponse = await GetAnalysisResponse(response); + analysisResponse.Should().NotBeNull(); + analysisResponse!.RoslynIssues.Should().BeEmpty(); + } + + private static async Task VerifyServerNotReachable(AnalysisRequestConfig analysisRequestConfig) where TException : Exception + { + var act = async () => await HttpRequester.SendRequest(analysisRequestConfig); + await act.Should().ThrowAsync(); + } + + private static async Task GetAnalysisResponse(HttpResponseMessage response) + { + var content = await response.Content.ReadAsStringAsync(); + var analysisResponse = JsonConvert.DeserializeObject(content); + return analysisResponse; + } + + private static void MockServerSettings( + IHttpServerSettings serverConfiguration, + int requestTimeout = 50, + int maxBodyLength = 1024) + { + serverConfiguration.MaxStartAttempts.Returns(3); + serverConfiguration.RequestMillisecondsTimeout.Returns(requestTimeout); + serverConfiguration.MaxRequestBodyBytes.Returns(maxBodyLength); + } + + private static void MockServerConfiguration(IHttpServerConfigurationProvider serverConfigurationProvider, int port) => serverConfigurationProvider.CurrentConfiguration.Port.Returns(port); + + private static void SimulateAnalysisRunsOutOfTime(IRoslynAnalysisService roslynAnalysisService) => + roslynAnalysisService + .When(x => x.AnalyzeAsync(Arg.Any(), Arg.Any())) + .Throw(new OperationCanceledException()); + + private static void SimulateAnalysisWithCallback(IRoslynAnalysisService roslynAnalysisService, Action callback) => + roslynAnalysisService.AnalyzeAsync(Arg.Any(), Arg.Any()) + .Returns(_ => + { + callback(); + return Task.FromResult(Enumerable.Empty()); + }); +} diff --git a/src/RoslynAnalyzerServer.IntegrationTests/RoslynAnalyzerServer.IntegrationTests.csproj b/src/RoslynAnalyzerServer.IntegrationTests/RoslynAnalyzerServer.IntegrationTests.csproj new file mode 100644 index 0000000000..1da3f5cac3 --- /dev/null +++ b/src/RoslynAnalyzerServer.IntegrationTests/RoslynAnalyzerServer.IntegrationTests.csproj @@ -0,0 +1,19 @@ + + + + + + SonarLint.VisualStudio.RoslynAnalyzerServer.IntegrationTests + SonarLint.VisualStudio.RoslynAnalyzerServer.IntegrationTests + enable + + + + + + + + + + + diff --git a/src/RoslynAnalyzerServer.IntegrationTests/packages.lock.json b/src/RoslynAnalyzerServer.IntegrationTests/packages.lock.json new file mode 100644 index 0000000000..4e6a6dffb0 --- /dev/null +++ b/src/RoslynAnalyzerServer.IntegrationTests/packages.lock.json @@ -0,0 +1,156 @@ +{ + "version": 1, + "dependencies": { + ".NETFramework,Version=v4.8": { + "FluentAssertions": { + "type": "Direct", + "requested": "[5.9.0, )", + "resolved": "5.9.0", + "contentHash": "JZxb5DuspmuH4A5p9bfj060evQnwIUkxxfFMG9F/pa7gl1Ry2r2nuQbu5G4UWGQQTtpTxPpVowqNDdBkA0G2Mw==" + }, + "FluentAssertions.Analyzers": { + "type": "Direct", + "requested": "[0.11.4, )", + "resolved": "0.11.4", + "contentHash": "zSCkwOgc5OyfMfEeMr9x0K7WCDf8i6VdF2RtCLN/4m6iebTtJQdeoJ9IS4/RyYHuLUYjrm0sd+siWbaSvSzRYQ==" + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[16.6.1, )", + "resolved": "16.6.1", + "contentHash": "zYAjfWzpxKb64P9ntReT1Xr8HdONZnpLVs12HIjXWo+UOCDpevP1UWRoaAgNysaD1/l3teBKvgbSeG9bRssfOQ==", + "dependencies": { + "Microsoft.CodeCoverage": "16.6.1" + } + }, + "Moq": { + "type": "Direct", + "requested": "[4.18.2, )", + "resolved": "4.18.2", + "contentHash": "SjxKYS5nX6prcaT8ZjbkONh3vnh0Rxru09+gQ1a07v4TM530Oe/jq3Q4dOZPfo1wq0LYmTgLOZKrqRfEx4auPw==", + "dependencies": { + "Castle.Core": "5.1.0", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "MSTest.TestAdapter": { + "type": "Direct", + "requested": "[1.4.0, )", + "resolved": "1.4.0", + "contentHash": "t2/rL9DG+cVAgPs98OGm2sbZ4FTgn+MGEan5P9NRAgqMV3+nYRKG7/5R0jY7lBMq9ISms+84MSqTHWs6QnPt4A==" + }, + "MSTest.TestFramework": { + "type": "Direct", + "requested": "[1.4.0, )", + "resolved": "1.4.0", + "contentHash": "kV/yZ0XLiOElsVeLT0GnNrsoKcPvVNOP6Cv2zkAiceJY0rpro0L+3t54bRApLwTg1mxlo4rLziBG7X6X69KcrQ==" + }, + "NSubstitute": { + "type": "Direct", + "requested": "[5.1.0, )", + "resolved": "5.1.0", + "contentHash": "ZCqOP3Kpp2ea7QcLyjMU4wzE+0wmrMN35PQMsdPOHYc2IrvjmusG9hICOiqiOTPKN0gJon6wyCn6ZuGHdNs9hQ==", + "dependencies": { + "Castle.Core": "5.1.1", + "System.Threading.Tasks.Extensions": "4.3.0" + } + }, + "NSubstitute.Analyzers.CSharp": { + "type": "Direct", + "requested": "[1.0.17, )", + "resolved": "1.0.17", + "contentHash": "Pwz0MD7CAM/G/fvJjM3ceOfI+S0IgjanHcK7evwyrW9qAWUG8fgiEXYfSX1/s3h2JUNDOw6ik0G8zp+RT61Y1g==" + }, + "BouncyCastle.Cryptography": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==" + }, + "Castle.Core": { + "type": "Transitive", + "resolved": "5.1.1", + "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==" + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "16.6.1", + "contentHash": "nBYXDgAZCfjsOVzlhMB5olGvX4dTDWB/gWaYS/MhgXBcCz8XJuVGqkfK8LmwlBR/eeUPE9Q/NFZNwlJyMZf0vg==" + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "FXkLXiK0sVVewcso0imKQoOxjoPAj42R8HtjjbSjVPAzwDfzoyoznWxgA3c38LDbN9SJux1xXoXYAhz98j7r2g==", + "dependencies": { + "System.Memory": "4.5.4" + } + }, + "System.IO.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "1h4krG51ZiW/CGzM8gtqrRW2oeG6WZDfPaj27suexL8PxBVahsUlUKMJrqI4kkh6ggHLSDd7MFeU8orpk6COZg==" + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Numerics.Vectors": "4.5.0", + "System.Runtime.CompilerServices.Unsafe": "4.5.3" + } + }, + "System.Numerics.Vectors": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "3TIsJhD1EiiT0w2CcDMN/iSSwnNnsrnbzeVHSKkaEgV85txMprmuO+Yq2AdSbeVGcg28pdNDTPK87tJhX7VFHw==" + }, + "System.Threading.Channels": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==", + "dependencies": { + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "4.5.3" + } + }, + "SonarLint.VisualStudio.Core": { + "type": "Project", + "dependencies": { + "BouncyCastle.Cryptography": "[2.4.0, )", + "Newtonsoft.Json": "[13.0.3, )", + "System.Collections.Immutable": "[5.0.0, )", + "System.IO.Abstractions": "[9.0.4, )", + "System.Threading.Channels": "[7.0.0, )" + } + }, + "SonarLint.VisualStudio.RoslynAnalyzerServer": { + "type": "Project", + "dependencies": { + "SonarLint.VisualStudio.Core": "[1.0.0, )" + } + } + } + } +} \ No newline at end of file diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/AnalysisConfigurationParametersCacheExtensionsTest.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/AnalysisConfigurationParametersCacheExtensionsTest.cs new file mode 100644 index 0000000000..4ce5cac31b --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/AnalysisConfigurationParametersCacheExtensionsTest.cs @@ -0,0 +1,165 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis.Configuration; + +[TestClass] +public class AnalysisConfigurationParametersCacheExtensionsTest +{ + private readonly AnalyzerInfoDto defaultAnalyzerInfoDto = new(false, false); + private readonly KeyValuePair disableRazorAnalysisProp = new("sonar.cs.internal.disableRazor", "true"); + private readonly ActiveRuleDto s101RuleWithParam = new("S101", new Dictionary { { "threshold", "3" } }); + + [TestMethod] + public void ShouldInvalidateCache_CacheIsNull_ReturnsTrue() + { + AnalysisConfigurationParametersCache? testSubject = null; + + var result = testSubject.ShouldInvalidateCache([], [], defaultAnalyzerInfoDto); + + result.Should().BeTrue(); + } + + [TestMethod] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void ShouldInvalidateCache_AnalyzerInfoDtoHasDifferentValues_ReturnsTrue(bool useSharpEnterprise, bool useVbNetEnterprise) + { + var testSubject = new AnalysisConfigurationParametersCache([], [], defaultAnalyzerInfoDto); + + var result = testSubject.ShouldInvalidateCache([], [], new(useSharpEnterprise, useVbNetEnterprise)); + + result.Should().BeTrue(); + } + + [TestMethod] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + [DataRow(false, false)] + public void ShouldInvalidateCache_AnalyzerInfoDtoHasSameValues_ReturnsFalse(bool useSharpEnterprise, bool useVbNetEnterprise) + { + var testSubject = new AnalysisConfigurationParametersCache([], [], new(useSharpEnterprise, useVbNetEnterprise)); + + var result = testSubject.ShouldInvalidateCache([], [], new(useSharpEnterprise, useVbNetEnterprise)); + + result.Should().BeFalse(); + } + + [TestMethod] + public void ShouldInvalidateCache_SameActiveRules_ReturnsFalse() + { + var activeRules = new Dictionary { { s101RuleWithParam.RuleId, s101RuleWithParam } }; + var sameActiveRules = new List { s101RuleWithParam with { Parameters = new Dictionary(s101RuleWithParam.Parameters) } }; + var testSubject = new AnalysisConfigurationParametersCache(activeRules, [], defaultAnalyzerInfoDto); + + var result = testSubject.ShouldInvalidateCache(sameActiveRules, [], defaultAnalyzerInfoDto); + + result.Should().BeFalse(); + } + + [TestMethod] + public void ShouldInvalidateCache_DifferentActiveRules_ReturnsTrue() + { + var activeRules = new Dictionary { { s101RuleWithParam.RuleId, s101RuleWithParam } }; + var newActiveRules = new List { new("S102", new Dictionary { { "timeout", "60" } }) }; + var testSubject = new AnalysisConfigurationParametersCache(activeRules, [], defaultAnalyzerInfoDto); + + var result = testSubject.ShouldInvalidateCache(newActiveRules, [], defaultAnalyzerInfoDto); + + result.Should().BeTrue(); + } + + [TestMethod] + public void ShouldInvalidateCache_SameRuleWithDifferentParameter_ReturnsTrue() + { + var activeRules = new Dictionary { { s101RuleWithParam.RuleId, s101RuleWithParam } }; + var newActiveRules = new List { s101RuleWithParam with { Parameters = new Dictionary { { "timeout", "60" } } } }; + var testSubject = new AnalysisConfigurationParametersCache(activeRules, [], defaultAnalyzerInfoDto); + + var result = testSubject.ShouldInvalidateCache(newActiveRules, [], defaultAnalyzerInfoDto); + + result.Should().BeTrue(); + } + + [TestMethod] + public void ShouldInvalidateCache_SameRuleWithDifferentParameters_ReturnsTrue() + { + var activeRules = new Dictionary { { s101RuleWithParam.RuleId, s101RuleWithParam } }; + var newActiveRules = new List { s101RuleWithParam with { Parameters = [] } }; + var testSubject = new AnalysisConfigurationParametersCache(activeRules, [], defaultAnalyzerInfoDto); + + var result = testSubject.ShouldInvalidateCache(newActiveRules, [], defaultAnalyzerInfoDto); + + result.Should().BeTrue(); + } + + [TestMethod] + public void ShouldInvalidateCache_SameRuleWithDifferentParameterValue_ReturnsTrue() + { + var activeRules = new Dictionary { { s101RuleWithParam.RuleId, s101RuleWithParam } }; + var newActiveRules = new List { s101RuleWithParam with { Parameters = new Dictionary { { "threshold", "5" } } } }; + var testSubject = new AnalysisConfigurationParametersCache(activeRules, [], defaultAnalyzerInfoDto); + + var result = testSubject.ShouldInvalidateCache(newActiveRules, [], defaultAnalyzerInfoDto); + + result.Should().BeTrue(); + } + + [TestMethod] + public void ShouldInvalidateCache_SameAnalysisProperties_ReturnsFalse() + { + var analysisProperties = new Dictionary { { disableRazorAnalysisProp.Key, disableRazorAnalysisProp.Value } }; + var sameAnalysisProperties = new Dictionary { { disableRazorAnalysisProp.Key, disableRazorAnalysisProp.Value } }; + var testSubject = new AnalysisConfigurationParametersCache([], analysisProperties, defaultAnalyzerInfoDto); + + var result = testSubject.ShouldInvalidateCache([], sameAnalysisProperties, defaultAnalyzerInfoDto); + + result.Should().BeFalse(); + } + + [TestMethod] + public void ShouldInvalidateCache_SameAnalysisPropertiesWithDifferentValue_ReturnsTrue() + { + var analysisProperties = new Dictionary { { disableRazorAnalysisProp.Key, disableRazorAnalysisProp.Value } }; + var newAnalysisProperties = new Dictionary { { disableRazorAnalysisProp.Key, "false" } }; + var testSubject = new AnalysisConfigurationParametersCache([], analysisProperties, defaultAnalyzerInfoDto); + + var result = testSubject.ShouldInvalidateCache([], newAnalysisProperties, defaultAnalyzerInfoDto); + + result.Should().BeTrue(); + } + + [TestMethod] + public void ShouldInvalidateCache_DifferentAnalysisProperties_ReturnsTrue() + { + var analysisProperties = new Dictionary { { disableRazorAnalysisProp.Key, disableRazorAnalysisProp.Value } }; + var newAnalysisProperties = new Dictionary(); + var testSubject = new AnalysisConfigurationParametersCache([], analysisProperties, defaultAnalyzerInfoDto); + + var result = testSubject.ShouldInvalidateCache([], newAnalysisProperties, defaultAnalyzerInfoDto); + + result.Should().BeTrue(); + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisConfigurationProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisConfigurationProviderTests.cs new file mode 100644 index 0000000000..f329aa6d22 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisConfigurationProviderTests.cs @@ -0,0 +1,350 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Synchronization; +using SonarLint.VisualStudio.Integration.TestInfrastructure; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis.Configuration; + +[TestClass] +public class RoslynAnalysisConfigurationProviderTests +{ + private static readonly ImmutableDictionary DefaultAnalyzers + = new Dictionary { { Language.CSharp, new AnalyzerAssemblyContents() } }.ToImmutableDictionary(); + private static readonly List DefaultActiveRules = []; + private static readonly Dictionary DefaultAnalysisProperties = []; + private static readonly AnalyzerInfoDto DefaultAnalyzerInfoDto = new(false, false); + private static readonly Dictionary DefaultRoslynAnalysisProfile = []; + + private IThreadHandling threadHandling = null!; + private IAsyncLock asyncLock = null!; + private ISonarLintXmlProvider sonarLintXmlProvider = null!; + private IRoslynAnalyzerProvider roslynAnalyzerProvider = null!; + private IRoslynAnalysisProfilesProvider analyzerProfilesProvider = null!; + private TestLogger testLogger = null!; + private RoslynAnalysisConfigurationProvider testSubject = null!; + + [TestInitialize] + public void TestInitialize() + { + var asyncLockFactory = Substitute.For(); + threadHandling = Substitute.ForPartsOf(); + sonarLintXmlProvider = Substitute.For(); + roslynAnalyzerProvider = Substitute.For(); + roslynAnalyzerProvider.LoadAndProcessAnalyzerAssemblies(DefaultAnalyzerInfoDto).Returns(DefaultAnalyzers); + asyncLock = Substitute.For(); + asyncLockFactory.Create().Returns(asyncLock); + + analyzerProfilesProvider = Substitute.For(); + analyzerProfilesProvider + .GetAnalysisProfilesByLanguage(Arg.Any>(), Arg.Any>(), Arg.Any>()) + .Returns(DefaultRoslynAnalysisProfile); + testLogger = Substitute.ForPartsOf(); + + testSubject = new RoslynAnalysisConfigurationProvider( + sonarLintXmlProvider, + roslynAnalyzerProvider, + analyzerProfilesProvider, + asyncLockFactory, + threadHandling, + testLogger); + } + + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public void Ctor_SetsLogContext() => + testLogger.Received(1).ForContext( + Resources.RoslynLogContext, + Resources.RoslynAnalysisLogContext, + Resources.RoslynAnalysisConfigurationLogContext); + + [TestMethod] + public async Task GetConfigurationAsync_CreatesConfigurationForEachLanguage() + { + var roslynAnalysisProfiles = new Dictionary + { + { + Language.CSharp, new RoslynAnalysisProfile( + CreateTestAnalyzers(1), + CreateTestCodeFixProviders(), + [CreateRuleConfiguration(Language.CSharp, "S001"), CreateRuleConfiguration(Language.CSharp, "S002", false)], + new() { { "sonar.cs.property", "value" } }) + }, + { + Language.VBNET, new RoslynAnalysisProfile( + CreateTestAnalyzers(2), + CreateTestCodeFixProviders(), + [CreateRuleConfiguration(Language.VBNET, "S001", false), CreateRuleConfiguration(Language.VBNET, "S002")], + new() { { "sonar.vbnet.property", "value" } }) + } + }; + + var xmlConfigurations = SetUpXmlConfigurations(roslynAnalysisProfiles); + analyzerProfilesProvider.GetAnalysisProfilesByLanguage(DefaultAnalyzers, DefaultActiveRules, DefaultAnalysisProperties) + .Returns(roslynAnalysisProfiles); + + var result = await testSubject.GetConfigurationAsync(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + + result.Keys.Should().BeEquivalentTo(roslynAnalysisProfiles.Keys); + foreach (var language in roslynAnalysisProfiles.Keys) + { + result.ContainsKey(language).Should().BeTrue(); + result[language].Analyzers.Should().BeEquivalentTo(roslynAnalysisProfiles[language].Analyzers); + result[language].CodeFixProvidersByRuleKey.Should().BeSameAs(roslynAnalysisProfiles[language].CodeFixProvidersByRuleKey); + result[language].DiagnosticOptions.Should().BeEquivalentTo(roslynAnalysisProfiles[language].Rules.ToDictionary(x => x.RuleId.RuleKey, x => x.ReportDiagnostic)); + result[language].SonarLintXml.Should().BeEquivalentTo(xmlConfigurations[language]); + } + } + + [TestMethod] + public async Task GetConfigurationAsync_NoAnalyzers_LogsAndExcludesLanguage() + { + var language = Language.CSharp; + var roslynAnalysisProfiles = new Dictionary + { + { + language, new RoslynAnalysisProfile( + ImmutableArray.Empty, + CreateTestCodeFixProviders(), + [CreateRuleConfiguration(language, "S001")], + []) + } + }; + + analyzerProfilesProvider.GetAnalysisProfilesByLanguage(DefaultAnalyzers, DefaultActiveRules, DefaultAnalysisProperties) + .Returns(roslynAnalysisProfiles); + + var result = await testSubject.GetConfigurationAsync(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + + result.Should().BeEmpty(); + testLogger.AssertPartialOutputStringExists(string.Format(Resources.RoslynAnalysisConfigurationNoAnalyzers, language.Name)); + } + + [TestMethod] + public async Task GetConfigurationAsync_NoActiveRules_LogsAndExcludesLanguage() + { + var language = Language.CSharp; + var roslynAnalysisProfiles = new Dictionary + { + { + language, new RoslynAnalysisProfile( + CreateTestAnalyzers(1), + CreateTestCodeFixProviders(), + [CreateRuleConfiguration(language, "S001", false), CreateRuleConfiguration(language, "S002", false)], + []) + } + }; + + analyzerProfilesProvider.GetAnalysisProfilesByLanguage(DefaultAnalyzers, DefaultActiveRules, DefaultAnalysisProperties) + .Returns(roslynAnalysisProfiles); + + var result = await testSubject.GetConfigurationAsync(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + + result.Should().BeEmpty(); + testLogger.AssertPartialOutputStringExists(string.Format(Resources.RoslynAnalysisConfigurationNoActiveRules, language.Name)); + } + + [TestMethod] + public async Task GetConfigurationAsync_NoAnalysisProfiles_ReturnsEmptyDictionary() + { + var result = await testSubject.GetConfigurationAsync(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + + result.Should().BeEmpty(); + } + + [TestMethod] + public async Task GetConfigurationAsync_MultipleCalls_SameActiveRules_Caches() + { + var activeRules = new List { new("S101", new Dictionary { { "threshold", "3" } }) }; + var sameActiveRules = new List { new("S101", new Dictionary { { "threshold", "3" } }) }; + + await testSubject.GetConfigurationAsync(activeRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(sameActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(sameActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), activeRules, DefaultAnalysisProperties); + } + + [TestMethod] + public async Task GetConfigurationAsync_MultipleCalls_DifferentActiveRules_InvalidatesCache() + { + var activeRules = new List { new("S101", new Dictionary { { "threshold", "3" } }) }; + var newActiveRules = new List { new("S102", new Dictionary { { "threshold", "3" } }) }; + + await testSubject.GetConfigurationAsync(activeRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(newActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(newActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), activeRules, DefaultAnalysisProperties); + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), newActiveRules, DefaultAnalysisProperties); + } + + [TestMethod] + public async Task GetConfigurationAsync_MultipleCalls_SameRuleWithDifferentParameter_InvalidatesCache() + { + var activeRules = new List { new("S101", new Dictionary { { "threshold", "3" } }) }; + var newActiveRules = new List { new("S101", new Dictionary { { "timeout", "60" } }) }; + + await testSubject.GetConfigurationAsync(activeRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(newActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(newActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), activeRules, DefaultAnalysisProperties); + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), newActiveRules, DefaultAnalysisProperties); + } + + [TestMethod] + public async Task GetConfigurationAsync_MultipleCalls_SameRuleWithDifferentParameters_InvalidatesCache() + { + var activeRules = new List { new("S101", new Dictionary { { "threshold", "3" } }) }; + var newActiveRules = new List { new("S101", []) }; + + await testSubject.GetConfigurationAsync(activeRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(newActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(newActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), activeRules, DefaultAnalysisProperties); + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), newActiveRules, DefaultAnalysisProperties); + } + + [TestMethod] + public async Task GetConfigurationAsync_MultipleCalls_SameRuleWithDifferentParameterValue_InvalidatesCache() + { + var activeRules = new List { new("S101", new Dictionary { { "threshold", "3" } }) }; + var newActiveRules = new List { new("S101", new Dictionary { { "threshold", "5" } }) }; + + await testSubject.GetConfigurationAsync(activeRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(newActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(newActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), activeRules, DefaultAnalysisProperties); + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), newActiveRules, DefaultAnalysisProperties); + } + + [TestMethod] + public async Task GetConfigurationAsync_MultipleCalls_SameAnalysisProperties_Caches() + { + var analysisProperties = new Dictionary { { "sonar.cs.internal.disableRazor", "true" } }; + var sameAnalysisProperties = new Dictionary { { "sonar.cs.internal.disableRazor", "true" } }; + + await testSubject.GetConfigurationAsync(DefaultActiveRules, analysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(DefaultActiveRules, sameAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(DefaultActiveRules, sameAnalysisProperties, DefaultAnalyzerInfoDto); + + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), DefaultActiveRules, analysisProperties); + } + + [TestMethod] + public async Task GetConfigurationAsync_MultipleCalls_SameAnalysisPropertyWithDifferentValue_InvalidatesCache() + { + var analysisProperties = new Dictionary { { "sonar.cs.internal.disableRazor", "true" } }; + var newAnalysisProperties = new Dictionary { { "sonar.cs.internal.disableRazor", "false" } }; + + await testSubject.GetConfigurationAsync(DefaultActiveRules, analysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(DefaultActiveRules, newAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(DefaultActiveRules, newAnalysisProperties, DefaultAnalyzerInfoDto); + + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), DefaultActiveRules, analysisProperties); + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), DefaultActiveRules, newAnalysisProperties); + } + + [TestMethod] + public async Task GetConfigurationAsync_MultipleCalls_DifferentAnalysisProperties_InvalidatesCache() + { + var analysisProperties = new Dictionary { { "sonar.cs.internal.disableRazor", "true" } }; + var newAnalysisProperties = new Dictionary(); + + await testSubject.GetConfigurationAsync(DefaultActiveRules, analysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(DefaultActiveRules, newAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(DefaultActiveRules, newAnalysisProperties, DefaultAnalyzerInfoDto); + + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), DefaultActiveRules, analysisProperties); + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), DefaultActiveRules, newAnalysisProperties); + } + + [TestMethod] + public async Task GetConfigurationAsync_MultipleCalls_Locks() + { + await testSubject.GetConfigurationAsync(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + + asyncLock.Received(3).AcquireAsync().IgnoreAwaitForAssert(); + } + + [TestMethod] + public async Task GetConfigurationAsync_RunsOnBackgroundThread() + { + await testSubject.GetConfigurationAsync(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + + threadHandling.Received(1).RunOnBackgroundThread(Arg.Any>>>()).IgnoreAwaitForAssert(); + } + + private Dictionary SetUpXmlConfigurations(Dictionary profiles) + { + var xmlConfigurations = new Dictionary(); + foreach (var profile in profiles) + { + var xml = SetUpXmlProvider(profile.Value); + xmlConfigurations.Add(profile.Key, xml); + } + return xmlConfigurations; + } + + private static RoslynRuleConfiguration CreateRuleConfiguration( + Language language, + string ruleKey, + bool isActive = true) => + new(new SonarCompositeRuleId(language.RepoInfo.Key, ruleKey), + isActive, + []); + + private static ImmutableArray CreateTestAnalyzers(int count) => Enumerable.Range(0, count).Select(_ => Substitute.For()).ToImmutableArray(); + + private static ImmutableDictionary> CreateTestCodeFixProviders() => + ImmutableDictionary>.Empty.Add("any", [Substitute.For()]); + + private SonarLintXmlConfigurationFile SetUpXmlProvider(RoslynAnalysisProfile profile) + { + var slxml = new SonarLintXmlConfigurationFile("any", "any"); + sonarLintXmlProvider.Create(profile).Returns(slxml); + return slxml; + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisProfilesProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisProfilesProviderTests.cs new file mode 100644 index 0000000000..1142a04b38 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisProfilesProviderTests.cs @@ -0,0 +1,116 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis.Configuration; + +[TestClass] +public class RoslynAnalysisProfilesProviderTests +{ + private RoslynAnalysisProfilesProvider testSubject = null!; + + [TestInitialize] + public void TestInitialize() => testSubject = new RoslynAnalysisProfilesProvider(); + + [TestMethod] + public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported(); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public void GetAnalysisProfilesByLanguage_EmptyInputs_ReturnsEmptyDictionary() + { + var supportedDiagnostics = ImmutableDictionary.Empty; + var activeRules = new List(); + Dictionary analysisProperties = []; + + var result = testSubject.GetAnalysisProfilesByLanguage(supportedDiagnostics, activeRules, analysisProperties); + + result.Should().BeEmpty(); + } + + [TestMethod] + public void GetAnalysisProfilesByLanguage_ReturnsFilteredRulesAndParameters() + { + var analyzerAssemblyContents = CreateSupportedDiagnosticsForLanguages(new() + { + { Language.CSharp, ([Substitute.For(), Substitute.For()], ["S001", "S002", "S003"], new(){{"S002", [Substitute.For()]}}) }, + { Language.VBNET, ([Substitute.For(), Substitute.For()], ["S001", "S002", "S003"], new (){{"S003", [Substitute.For()]}}) }, + }); + List activeRules = + [ + new("csharpsquid:S001", new Dictionary { { "param1", "value1" } }), + new("csharpsquid:S003", []), + new("csharpsquid:SUNSUPPORTED", []), + new("vbnet:S002", new Dictionary { { "param2", "value2" } }) + ]; + var analysisProperties = new Dictionary { { "sonar.cs.property1", "value1" }, { "sonar.vbnet.property2", "value2" }, { "someotherkey", "value" } }; + + var result = testSubject.GetAnalysisProfilesByLanguage(analyzerAssemblyContents, activeRules, analysisProperties); + + result.Keys.Should().BeEquivalentTo(Language.CSharp, Language.VBNET); + ValidateProfile( + result[Language.CSharp], + analyzerAssemblyContents[Language.CSharp].Analyzers, + analyzerAssemblyContents[Language.CSharp].CodeFixProvidersByRuleKey, + [ + CreateRuleConfiguration(Language.CSharp, "S001", new() { { "param1", "value1" } }), + CreateRuleConfiguration(Language.CSharp, "S002", isActive: false), + CreateRuleConfiguration(Language.CSharp, "S003", []) + ], + new() { { "sonar.cs.property1", "value1" } }); + ValidateProfile( + result[Language.VBNET], + analyzerAssemblyContents[Language.VBNET].Analyzers, + analyzerAssemblyContents[Language.VBNET].CodeFixProvidersByRuleKey, + [ + CreateRuleConfiguration(Language.VBNET, "S001", isActive: false), + CreateRuleConfiguration(Language.VBNET, "S002", parameters: new() { { "param2", "value2" } }), + CreateRuleConfiguration(Language.VBNET, "S003", isActive: false) + ], + new Dictionary { { "sonar.vbnet.property2", "value2" } }); + } + + private static void ValidateProfile(RoslynAnalysisProfile profile, IEnumerable diagnosticAnalyzers, ImmutableDictionary> codeFixProviders, List rules, Dictionary analysisProperties) => + profile.Should().BeEquivalentTo(new RoslynAnalysisProfile(diagnosticAnalyzers.ToImmutableArray(), codeFixProviders, rules, analysisProperties), options => options.ComparingByMembers().ComparingByMembers()); + + private static RoslynRuleConfiguration CreateRuleConfiguration( + Language language, + string ruleKey, + Dictionary? parameters = null, + bool isActive = true) => + new(new SonarCompositeRuleId(language.RepoInfo.Key, ruleKey), + isActive, + parameters); + + private static ImmutableDictionary CreateSupportedDiagnosticsForLanguages( + Dictionary> CodeFixProviders)> contents) => + contents.ToImmutableDictionary( + x => x.Key, + y => new AnalyzerAssemblyContents(y.Value.analyzers.ToImmutableArray(), y.Value.RuleKeys.ToImmutableHashSet(), y.Value.CodeFixProviders.ToImmutableDictionary())); +} diff --git a/src/Infrastructure.VS.UnitTests/Roslyn/AnalyzerAssemblyLoaderFactoryTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerLoaderTests.cs similarity index 52% rename from src/Infrastructure.VS.UnitTests/Roslyn/AnalyzerAssemblyLoaderFactoryTests.cs rename to src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerLoaderTests.cs index 5626430707..3aeb13e25c 100644 --- a/src/Infrastructure.VS.UnitTests/Roslyn/AnalyzerAssemblyLoaderFactoryTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerLoaderTests.cs @@ -18,39 +18,29 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using SonarLint.VisualStudio.Infrastructure.VS.Roslyn; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; +using SonarLint.VisualStudio.TestInfrastructure; -namespace SonarLint.VisualStudio.Infrastructure.VS.UnitTests.Roslyn; +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis.Configuration; [TestClass] -public class AnalyzerAssemblyLoaderFactoryTests +public class RoslynAnalyzerLoaderTests { - private AnalyzerAssemblyLoaderFactory testSubject; - - [TestInitialize] - public void TestInitialize() - { - testSubject = new AnalyzerAssemblyLoaderFactory(); - } - [TestMethod] - public void MefCtor_CheckExports() - { - MefTestHelpers.CheckTypeCanBeImported(); - } + public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport()); [TestMethod] - public void Mef_CheckIsSingleton() - { - MefTestHelpers.CheckIsSingletonMefComponent(); - } + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); [TestMethod] - public void Create_CachesLoader() + public void Ctor_SetsLogContext() { - var loader1 = testSubject.Create(); - var loader2 = testSubject.Create(); + var logger = Substitute.For(); + + _ = new RoslynAnalyzerLoader(logger); - Assert.AreSame(loader1, loader2); + logger.Received().ForContext(Resources.RoslynLogContext, Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisAnalyzerLoaderLogContext); } } diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerProviderTests.cs new file mode 100644 index 0000000000..27733ed2a4 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerProviderTests.cs @@ -0,0 +1,302 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis.Configuration; + +[TestClass] +public class RoslynAnalyzerProviderTests +{ + private const string CsharpAnalyzerPath = "c:\\analyzers\\csharp.dll"; + private const string CsharpEnterpriseAnalyzerPath = "c:\\analyzers\\csharp.enterprise.dll"; + private const string VbAnalyzerPath = "c:\\analyzers\\vb.dll"; + private const string VbEnterpriseAnalyzerPath = "c:\\analyzers\\vb.enterprise.dll"; + private readonly DiagnosticAnalyzer CsharpAnalyzer = CreateAnalyzerWithDiagnostic(); + private readonly DiagnosticAnalyzer CsharpEnterpriseAnalyzer = CreateAnalyzerWithDiagnostic(); + private readonly DiagnosticAnalyzer VbAnalyzer = CreateAnalyzerWithDiagnostic(); + private readonly DiagnosticAnalyzer VbEnterpriseAnalyzer = CreateAnalyzerWithDiagnostic(); + private static readonly AnalyzerInfoDto DefaultAnalyzerInfoDto = new(false, false); + + private IEmbeddedDotnetAnalyzersLocator analyzersLocator = null!; + private IRoslynAnalyzerLoader roslynAnalyzerLoader = null!; + private RoslynAnalyzerProvider testSubject = null!; + + [TestInitialize] + public void TestInitialize() + { + analyzersLocator = Substitute.For(); + roslynAnalyzerLoader = Substitute.For(); + testSubject = new RoslynAnalyzerProvider(analyzersLocator, roslynAnalyzerLoader); + } + + [TestMethod] + public void MefCtor_IRoslynAnalyzerProvider_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_IRoslynAnalyzerAssemblyContentsLoader_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public void LoadAndProcessAnalyzerAssemblies_NoAnalyzers_ReturnsEmptyDictionary() + { + analyzersLocator.GetAnalyzerFullPathsByLicensedLanguage().Returns(new Dictionary>()); + + var result = testSubject.LoadAndProcessAnalyzerAssemblies(DefaultAnalyzerInfoDto); + + result.Should().BeEmpty(); + } + + [TestMethod] + public void LoadAndProcessAnalyzerAssemblies_WithAnalyzers_LoadsAnalyzersAndReturnsCorrectDictionary() + { + analyzersLocator.GetAnalyzerFullPathsByLicensedLanguage() + .Returns(new Dictionary> { { new(Language.CSharp, false), [CsharpAnalyzerPath] }, { new(Language.VBNET, false), [VbAnalyzerPath] } }); + var csharpAnalyzer = CreateAnalyzerWithDiagnostic("CS0001"); + var vbAnalyzer = CreateAnalyzerWithDiagnostic("VB0001"); + var csharpCodeFixer = CreateCodeFixProviderWithDiagnostics("CS0001"); + var vbCodeFixer = CreateCodeFixProviderWithDiagnostics("VB0001"); + roslynAnalyzerLoader.LoadAnalyzerAssembly(CsharpAnalyzerPath).Returns(new LoadedAnalyzerClasses([csharpAnalyzer], [csharpCodeFixer])); + roslynAnalyzerLoader.LoadAnalyzerAssembly(VbAnalyzerPath).Returns(new LoadedAnalyzerClasses([vbAnalyzer], [vbCodeFixer])); + + var result = testSubject.LoadAndProcessAnalyzerAssemblies(DefaultAnalyzerInfoDto); + + result.Keys.Should().BeEquivalentTo(Language.CSharp, Language.VBNET); + result[Language.CSharp].Analyzers.Should().BeEquivalentTo(csharpAnalyzer); + result[Language.CSharp].SupportedRuleKeys.Should().BeEquivalentTo("CS0001"); + result[Language.CSharp].CodeFixProvidersByRuleKey.Should().BeEquivalentTo( + new Dictionary> { { "CS0001", [csharpCodeFixer] } }); + + result[Language.VBNET].Analyzers.Should().BeEquivalentTo(vbAnalyzer); + result[Language.VBNET].SupportedRuleKeys.Should().BeEquivalentTo("VB0001"); + result[Language.VBNET].CodeFixProvidersByRuleKey.Should().BeEquivalentTo( + new Dictionary> { { "VB0001", [vbCodeFixer] } }); + } + + [TestMethod] + public void LoadAndProcessAnalyzerAssemblies_IgnoresDuplicateIdsForTheSameLanguage() + { + analyzersLocator.GetAnalyzerFullPathsByLicensedLanguage() + .Returns(new Dictionary> { { new(Language.CSharp, false), [CsharpAnalyzerPath] }, { new(Language.VBNET, false), [VbAnalyzerPath] } }); + var csharpAnalyzer1 = CreateAnalyzerWithDiagnostic("S001", "SDUPLICATE"); + var csharpAnalyzer2 = CreateAnalyzerWithDiagnostic("S002", "SDUPLICATE"); + var vbAnalyzer = CreateAnalyzerWithDiagnostic("S001", "S002"); + roslynAnalyzerLoader.LoadAnalyzerAssembly(CsharpAnalyzerPath).Returns(new LoadedAnalyzerClasses([csharpAnalyzer1, csharpAnalyzer2], [])); + roslynAnalyzerLoader.LoadAnalyzerAssembly(VbAnalyzerPath).Returns(new LoadedAnalyzerClasses([vbAnalyzer], [])); + + var result = testSubject.LoadAndProcessAnalyzerAssemblies(DefaultAnalyzerInfoDto); + + result.Keys.Should().BeEquivalentTo(Language.CSharp, Language.VBNET); + result[Language.CSharp].SupportedRuleKeys.Should().BeEquivalentTo("S001", "SDUPLICATE", "S002"); + result[Language.VBNET].SupportedRuleKeys.Should().BeEquivalentTo("S001", "S002"); + } + + [TestMethod] + public void LoadAndProcessAnalyzerAssemblies_MultipleAnalyzersPerLanguage_CombinesAllRules() + { + const string csharpAnalyzerPath2 = "c:\\analyzers\\csharp2.dll"; + analyzersLocator.GetAnalyzerFullPathsByLicensedLanguage() + .Returns(new Dictionary> { { new(Language.CSharp, false), [CsharpAnalyzerPath, csharpAnalyzerPath2] } }); + var csharpAnalyzer1 = CreateAnalyzerWithDiagnostic("S001"); + var csharpAnalyzer2 = CreateAnalyzerWithDiagnostic("S002", "S003"); + var csharpAnalyzer3 = CreateAnalyzerWithDiagnostic("S004"); + roslynAnalyzerLoader.LoadAnalyzerAssembly(CsharpAnalyzerPath).Returns(new LoadedAnalyzerClasses([csharpAnalyzer1], [])); + roslynAnalyzerLoader.LoadAnalyzerAssembly(csharpAnalyzerPath2).Returns(new LoadedAnalyzerClasses([csharpAnalyzer2, csharpAnalyzer3], [])); + + var result = testSubject.LoadAndProcessAnalyzerAssemblies(DefaultAnalyzerInfoDto); + + result.Keys.Should().BeEquivalentTo(Language.CSharp); + result[Language.CSharp].Analyzers.Should().BeEquivalentTo(csharpAnalyzer1, csharpAnalyzer2, csharpAnalyzer3); + result[Language.CSharp].SupportedRuleKeys.Should().BeEquivalentTo("S001", "S002", "S003", "S004"); + } + + [TestMethod] + public void LoadAndProcessAnalyzerAssemblies_NoCodeFixProviders_ReturnsEmptyMap() + { + var csharpAnalyzer = CreateAnalyzerWithDiagnostic("S001"); + MockCodeProvidersForCsharp(csharpAnalyzer, []); + + var result = testSubject.LoadAndProcessAnalyzerAssemblies(DefaultAnalyzerInfoDto); + + result[Language.CSharp].CodeFixProvidersByRuleKey.Should().BeEmpty(); + } + + [TestMethod] + public void LoadAndProcessAnalyzerAssemblies_CodeFixProviderWithMultipleDiagnostics_AddedToAllMappings() + { + var csharpAnalyzer = CreateAnalyzerWithDiagnostic("S001", "S002", "S003"); + var codeFixProvider = CreateCodeFixProviderWithDiagnostics("S001", "S002"); + MockCodeProvidersForCsharp(csharpAnalyzer, codeFixProvider); + + var result = testSubject.LoadAndProcessAnalyzerAssemblies(DefaultAnalyzerInfoDto); + + result[Language.CSharp].CodeFixProvidersByRuleKey.Should().BeEquivalentTo( + new Dictionary> { { "S001", [codeFixProvider] }, { "S002", [codeFixProvider] } }); + } + + [TestMethod] + public void LoadAndProcessAnalyzerAssemblies_MultipleCodeFixProvidersForSameId_AllAddedToSameCollection() + { + var csharpAnalyzer = CreateAnalyzerWithDiagnostic("S001"); + var codeFixProvider1 = CreateCodeFixProviderWithDiagnostics("S001"); + var codeFixProvider2 = CreateCodeFixProviderWithDiagnostics("S001"); + MockCodeProvidersForCsharp(csharpAnalyzer, codeFixProvider1, codeFixProvider2); + + var result = testSubject.LoadAndProcessAnalyzerAssemblies(DefaultAnalyzerInfoDto); + + result[Language.CSharp].CodeFixProvidersByRuleKey.Should().BeEquivalentTo( + new Dictionary> { { "S001", [codeFixProvider1, codeFixProvider2] } }); + } + + [TestMethod] + public void LoadAndProcessAnalyzerAssemblies_BothEnterprise_ReturnsEnterpriseDlls() + { + MockAnalyzerFullPathsByLicensedLanguage(); + var analyzerInfo = new AnalyzerInfoDto(ShouldUseCsharpEnterprise: true, ShouldUseVbEnterprise: true); + + var result = testSubject.LoadAndProcessAnalyzerAssemblies(analyzerInfo); + + result[Language.CSharp].Analyzers.Should().BeEquivalentTo(new List { CsharpEnterpriseAnalyzer }); + result[Language.VBNET].Analyzers.Should().BeEquivalentTo(new List { VbEnterpriseAnalyzer }); + } + + [TestMethod] + public void LoadAndProcessAnalyzerAssemblies_BothBasic_ReturnsBasicDlls() + { + MockAnalyzerFullPathsByLicensedLanguage(); + var analyzerInfo = new AnalyzerInfoDto(ShouldUseCsharpEnterprise: false, ShouldUseVbEnterprise: false); + + var result = testSubject.LoadAndProcessAnalyzerAssemblies(analyzerInfo); + + result[Language.CSharp].Analyzers.Should().BeEquivalentTo(new List { CsharpAnalyzer }); + result[Language.VBNET].Analyzers.Should().BeEquivalentTo(new List { VbAnalyzer }); + } + + [TestMethod] + public void LoadAndProcessAnalyzerAssemblies_OnlyCsharpEnterprise_ReturnsCsharpEnterpriseAndVbBasic() + { + MockAnalyzerFullPathsByLicensedLanguage(); + var analyzerInfo = new AnalyzerInfoDto(ShouldUseCsharpEnterprise: true, ShouldUseVbEnterprise: false); + + var result = testSubject.LoadAndProcessAnalyzerAssemblies(analyzerInfo); + + result[Language.CSharp].Analyzers.Should().BeEquivalentTo(new List { CsharpEnterpriseAnalyzer }); + result[Language.VBNET].Analyzers.Should().BeEquivalentTo(new List { VbAnalyzer }); + } + + [TestMethod] + public void LoadAndProcessAnalyzerAssemblies_OnlyVbEnterprise_ReturnsVbEnterpriseAndCsharpBasic() + { + MockAnalyzerFullPathsByLicensedLanguage(); + var analyzerInfo = new AnalyzerInfoDto(ShouldUseCsharpEnterprise: false, ShouldUseVbEnterprise: true); + + var result = testSubject.LoadAndProcessAnalyzerAssemblies(analyzerInfo); + + result[Language.CSharp].Analyzers.Should().BeEquivalentTo(new List { CsharpAnalyzer }); + result[Language.VBNET].Analyzers.Should().BeEquivalentTo(new List { VbEnterpriseAnalyzer }); + } + + [TestMethod] + public void LoadAndProcessAnalyzerAssemblies_MultipleCalls_AnalyzerAssemblyContentsAreCached() + { + MockCodeProvidersForCsharp(CsharpAnalyzer); + + testSubject.LoadAndProcessAnalyzerAssemblies(DefaultAnalyzerInfoDto); + testSubject.LoadAndProcessAnalyzerAssemblies(DefaultAnalyzerInfoDto); + testSubject.LoadAndProcessAnalyzerAssemblies(DefaultAnalyzerInfoDto); + + analyzersLocator.Received(1).GetAnalyzerFullPathsByLicensedLanguage(); + roslynAnalyzerLoader.Received(1).LoadAnalyzerAssembly(Arg.Any()); + } + + [TestMethod] + public void LoadRoslynAnalyzerAssemblyContentsIfNeeded_MultipleCalls_AnalyzerAssemblyContentsAreCached() + { + MockCodeProvidersForCsharp(CsharpAnalyzer); + + testSubject.LoadRoslynAnalyzerAssemblyContentsIfNeeded(); + testSubject.LoadRoslynAnalyzerAssemblyContentsIfNeeded(); + testSubject.LoadRoslynAnalyzerAssemblyContentsIfNeeded(); + + analyzersLocator.Received(1).GetAnalyzerFullPathsByLicensedLanguage(); + roslynAnalyzerLoader.Received(1).LoadAnalyzerAssembly(Arg.Any()); + } + + private static DiagnosticAnalyzer CreateAnalyzerWithDiagnostic(params string[] diagnosticIds) + { + var analyzer = Substitute.For(); + analyzer.SupportedDiagnostics.Returns(diagnosticIds.Select(CreateDiagnosticDescriptor).ToImmutableArray()); + return analyzer; + } + + private static CodeFixProvider CreateCodeFixProviderWithDiagnostics(params string[] diagnosticIds) + { + var codeFixProvider = Substitute.For(); + codeFixProvider.FixableDiagnosticIds.Returns(diagnosticIds.ToImmutableArray()); + return codeFixProvider; + } + + private static DiagnosticDescriptor CreateDiagnosticDescriptor(string id) => + new( + id, + "any title", + "any message", + "any category", + DiagnosticSeverity.Warning, + true); + + private void MockCodeProvidersForCsharp(DiagnosticAnalyzer csharpAnalyzer, params CodeFixProvider[] codeFixProviders) + { + analyzersLocator.GetAnalyzerFullPathsByLicensedLanguage() + .Returns(new Dictionary> { { new(Language.CSharp, false), [CsharpAnalyzerPath] } }); + roslynAnalyzerLoader.LoadAnalyzerAssembly(CsharpAnalyzerPath).Returns(new LoadedAnalyzerClasses([csharpAnalyzer], codeFixProviders)); + } + + private void MockAnalyzerFullPathsByLicensedLanguage() + { + roslynAnalyzerLoader.LoadAnalyzerAssembly(CsharpAnalyzerPath).Returns(new LoadedAnalyzerClasses([CsharpAnalyzer], [])); + roslynAnalyzerLoader.LoadAnalyzerAssembly(CsharpEnterpriseAnalyzerPath).Returns(new LoadedAnalyzerClasses([CsharpEnterpriseAnalyzer], [])); + roslynAnalyzerLoader.LoadAnalyzerAssembly(VbAnalyzerPath).Returns(new LoadedAnalyzerClasses([VbAnalyzer], [])); + roslynAnalyzerLoader.LoadAnalyzerAssembly(VbEnterpriseAnalyzerPath).Returns(new LoadedAnalyzerClasses([VbEnterpriseAnalyzer], [])); + analyzersLocator.GetAnalyzerFullPathsByLicensedLanguage() + .Returns(new Dictionary> + { + { new(Language.CSharp, false), [CsharpAnalyzerPath] }, + { new(Language.CSharp, true), [CsharpEnterpriseAnalyzerPath] }, + { new(Language.VBNET, false), [VbAnalyzerPath] }, + { new(Language.VBNET, true), [VbEnterpriseAnalyzerPath] }, + }); + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynRuleConfigurationTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynRuleConfigurationTests.cs new file mode 100644 index 0000000000..9892658384 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynRuleConfigurationTests.cs @@ -0,0 +1,74 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.CodeAnalysis; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis.Configuration; + +[TestClass] +public class RoslynRuleConfigurationTests +{ + [TestMethod] + public void Constructor_InitializesProperties() + { + var ruleId = new SonarCompositeRuleId("cpp", "rule1"); + var parameters = new Dictionary { { "param1", "value1" }, { "param2", "value2" } }; + var isActive = true; + + var testSubject = new RoslynRuleConfiguration(ruleId, isActive, parameters); + + testSubject.RuleId.Should().Be(ruleId); + testSubject.IsActive.Should().Be(isActive); + testSubject.Parameters.Should().BeSameAs(parameters); + } + + [TestMethod] + public void Constructor_NullParameters_AcceptsNull() + { + var ruleId = new SonarCompositeRuleId("cpp", "rule1"); + var isActive = true; + + var testSubject = new RoslynRuleConfiguration(ruleId, isActive, null); + + testSubject.RuleId.Should().Be(ruleId); + testSubject.IsActive.Should().Be(isActive); + testSubject.Parameters.Should().BeNull(); + } + + [TestMethod] + public void ReportDiagnostic_WhenActive_ReturnsWarn() + { + var ruleId = new SonarCompositeRuleId("cpp", "rule1"); + var testSubject = new RoslynRuleConfiguration(ruleId, true, null); + + testSubject.ReportDiagnostic.Should().Be(ReportDiagnostic.Warn); + } + + [TestMethod] + public void ReportDiagnostic_WhenInactive_ReturnsSuppress() + { + var ruleId = new SonarCompositeRuleId("cpp", "rule1"); + var testSubject = new RoslynRuleConfiguration(ruleId, false, null); + + testSubject.ReportDiagnostic.Should().Be(ReportDiagnostic.Suppress); + } +} diff --git a/src/Integration.UnitTests/CSharpVB/SonarLintConfigurationXmlSerializerTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/SonarLintConfigurationXmlSerializerTests.cs similarity index 81% rename from src/Integration.UnitTests/CSharpVB/SonarLintConfigurationXmlSerializerTests.cs rename to src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/SonarLintConfigurationXmlSerializerTests.cs index fc2027fdd8..39ab9f75a1 100644 --- a/src/Integration.UnitTests/CSharpVB/SonarLintConfigurationXmlSerializerTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/SonarLintConfigurationXmlSerializerTests.cs @@ -19,15 +19,15 @@ */ using SonarLint.VisualStudio.Core.CSharpVB; -using SonarLint.VisualStudio.Integration.CSharpVB; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; using SonarLint.VisualStudio.TestInfrastructure; -namespace SonarLint.VisualStudio.Integration.UnitTests.CSharpVB; +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis.Configuration; [TestClass] public class SonarLintConfigurationXmlSerializerTests { - private SonarLintConfigurationXmlSerializer testSubject; + private SonarLintConfigurationXmlSerializer testSubject = null!; [TestInitialize] public void TestInitialize() => testSubject = new SonarLintConfigurationXmlSerializer(); @@ -46,10 +46,7 @@ public void Generate_Serialized_ReturnsExpectedXml() Settings = [ new SonarLintKeyValuePair { Key = "sonar.cs.prop1", Value = "value 1" }, - new SonarLintKeyValuePair { Key = "sonar.cs.prop2", Value = "value 2" }, - new SonarLintKeyValuePair { Key = "sonar.exclusions", Value = "**/path1" }, - new SonarLintKeyValuePair { Key = "sonar.global.exclusions", Value = "**/path2" }, - new SonarLintKeyValuePair { Key = "sonar.inclusions", Value = "**/path3" }, + new SonarLintKeyValuePair { Key = "sonar.cs.prop2", Value = "value 2" } ], Rules = [ @@ -69,6 +66,16 @@ public void Generate_Serialized_ReturnsExpectedXml() [ new SonarLintKeyValuePair { Key = "x", Value = "y y" } ] + }, + new SonarLintRule + { + Key = "s777", + Parameters = [] + }, + new SonarLintRule + { + Key = "s888", + Parameters = null } ] }; @@ -88,18 +95,6 @@ public void Generate_Serialized_ReturnsExpectedXml() sonar.cs.prop2 value 2 - - sonar.exclusions - **/path1 - - - sonar.global.exclusions - **/path2 - - - sonar.inclusions - **/path3 - @@ -124,6 +119,13 @@ public void Generate_Serialized_ReturnsExpectedXml() + + s777 + + + + s888 + """); diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/SonarLintXmlProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/SonarLintXmlProviderTests.cs new file mode 100644 index 0000000000..3961faa936 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/SonarLintXmlProviderTests.cs @@ -0,0 +1,137 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.CSharpVB; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis.Configuration; + +[TestClass] +public class SonarLintXmlProviderTests +{ + private const string SerializedXml = "serialized xml content"; + private ISonarLintConfigurationXmlSerializer sonarLintConfigurationXmlSerializer = null!; + private SonarLintXmlProvider testSubject = null!; + private static readonly RoslynRuleConfiguration RuleWithoutParameters = CreateRuleConfig("rule1"); + private static readonly RoslynRuleConfiguration RuleWithParameters = CreateRuleConfig("rule2", parameters: new Dictionary { { "param1", "paramValue1" }, { "param2", "paramValue2" } }); + + [TestInitialize] + public void TestInitialize() + { + sonarLintConfigurationXmlSerializer = Substitute.For(); + sonarLintConfigurationXmlSerializer.Serialize(Arg.Any()).Returns(SerializedXml); + testSubject = new SonarLintXmlProvider(sonarLintConfigurationXmlSerializer); + } + + [TestMethod] + public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported(MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public void Create_ReturnsExpectedXml() + { + var profile = new RoslynAnalysisProfile(default, null!, [], []); + + var result = testSubject.Create(profile); + + result.Should().NotBeNull(); + result.Path.Should().Be(Path.Combine(Path.GetTempPath(), "SonarLint.xml")); + result.GetText().ToString().Should().Be(SerializedXml); + sonarLintConfigurationXmlSerializer.Received(1).Serialize(Arg.Any()); + } + + [TestMethod] + public void Create_WithMultipleRulesAndProperties_ExpectedConfigurationSerialized() + { + var analysisProperties = new Dictionary { { "prop1", "value1" }, { "prop2", "value2" } }; + var result = testSubject.Create(new RoslynAnalysisProfile(default, null!, [RuleWithoutParameters, RuleWithParameters], analysisProperties)); + + result.Should().NotBeNull(); + + var sonarLintConfiguration = sonarLintConfigurationXmlSerializer.ReceivedCalls().Single().GetArguments()[0] as SonarLintConfiguration; + sonarLintConfiguration!.Rules.Count.Should().Be(2); + sonarLintConfiguration!.Settings.Should().BeEquivalentTo([ + new SonarLintKeyValuePair { Key = "prop1", Value = "value1" }, + new SonarLintKeyValuePair { Key = "prop2", Value = "value2" } + ]); + } + + [TestMethod] + public void Create_WithRuleNoParametersNoProperties_SerializesCorrectRules() + { + var profile = new RoslynAnalysisProfile(default, null!, [RuleWithoutParameters], []); + + var result = testSubject.Create(profile); + + result.Should().NotBeNull(); + var sonarLintConfiguration = sonarLintConfigurationXmlSerializer.ReceivedCalls().Single().GetArguments()[0] as SonarLintConfiguration; + sonarLintConfiguration.Should().BeEquivalentTo(new SonarLintConfiguration { Rules = [new SonarLintRule { Key = "rule1", Parameters = [] }], Settings = [] }); + } + + [TestMethod] + public void Create_WithRuleWithParameters_SerializesCorrectRules() + { + var profile = new RoslynAnalysisProfile(default, null!, [RuleWithParameters], []); + + var result = testSubject.Create(profile); + + result.Should().NotBeNull(); + var sonarLintConfiguration = sonarLintConfigurationXmlSerializer.ReceivedCalls().Single().GetArguments()[0] as SonarLintConfiguration; + sonarLintConfiguration.Should().BeEquivalentTo(new SonarLintConfiguration + { + Rules = + [ + new SonarLintRule + { + Key = "rule2", Parameters = [new SonarLintKeyValuePair { Key = "param1", Value = "paramValue1" }, new SonarLintKeyValuePair { Key = "param2", Value = "paramValue2" }] + } + ], + Settings = [] + }); + } + + [TestMethod] + public void Create_WithInactiveRules_OnlyIncludesActiveRules() + { + const string inactiveRuleKey = "inactiveRule"; + var rules = new List { RuleWithoutParameters, CreateRuleConfig(inactiveRuleKey, false), RuleWithParameters }; + var profile = new RoslynAnalysisProfile(default, null!, rules, []); + + var result = testSubject.Create(profile); + + result.Should().NotBeNull(); + var sonarLintConfiguration = (SonarLintConfiguration)sonarLintConfigurationXmlSerializer.ReceivedCalls().Single().GetArguments()[0]!; + sonarLintConfiguration.Rules.Count.Should().Be(2); + sonarLintConfiguration.Rules.Select(x => x.Key).Should().NotContain(inactiveRuleKey); + } + + private static RoslynRuleConfiguration CreateRuleConfig( + string ruleKey, + bool isActive = true, + Dictionary? parameters = null) => + new( + new SonarCompositeRuleId(Language.CSharp.RepoInfo.Key, ruleKey), + isActive, + parameters); +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticDuplicatesComparerTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticDuplicatesComparerTests.cs new file mode 100644 index 0000000000..6f83a8ec41 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticDuplicatesComparerTests.cs @@ -0,0 +1,130 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.SLCore.Common.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis; + +[TestClass] +public class DiagnosticDuplicatesComparerTests +{ + private readonly DiagnosticDuplicatesComparer testSubject = DiagnosticDuplicatesComparer.Instance; + private readonly RoslynIssue diagnostic1 = CreateDiagnostic("rule1", new FileUri("file:///a/file1.cs"), 1, 1, 1, 10); + + [TestMethod] + public void Equals_SameReference_ReturnsTrue() + { + var result = testSubject.Equals(diagnostic1, diagnostic1); + + result.Should().BeTrue(); + } + + [TestMethod] + public void Equals_FirstArgumentNull_ReturnsFalse() + { + var result = testSubject.Equals(null, diagnostic1); + + result.Should().BeFalse(); + } + + [TestMethod] + public void Equals_SecondArgumentNull_ReturnsFalse() + { + var result = testSubject.Equals(diagnostic1, null); + + result.Should().BeFalse(); + } + + [TestMethod] + public void Equals_SameRuleKeyAndLocation_ReturnsTrue() + { + var diagnostic2 = CreateDiagnostic("rule1", new FileUri("file:///a/file1.cs"), 1, 1, 1, 10); + + var result = testSubject.Equals(diagnostic1, diagnostic2); + + result.Should().BeTrue(); + } + + + [TestMethod] + public void Equals_SameRuleKeyAndLocation_MessageIsIgnored() + { + var diagnostic2 = CreateDiagnostic("rule1", new FileUri("file:///a/file1.cs"), 1, 1, 1, 10, "some different message"); + + var result = testSubject.Equals(diagnostic1, diagnostic2); + + result.Should().BeTrue(); + } + + [TestMethod] + [DataRow("rule2", "file:///a/file1.cs", 1, 1, 1, 10, DisplayName = "Different RuleKey")] + [DataRow("rule1", "file:///a/file2.cs", 1, 1, 1, 10, DisplayName = "Different FilePath")] + [DataRow("rule1", "file:///a/file1.cs", 2, 1, 1, 10, DisplayName = "Different StartLine")] + [DataRow("rule1", "file:///a/file1.cs", 1, 1, 2, 10, DisplayName = "Different EndLine")] + [DataRow("rule1", "file:///a/file1.cs", 1, 2, 1, 10, DisplayName = "Different StartLineOffset")] + [DataRow("rule1", "file:///a/file1.cs", 1, 1, 1, 11, DisplayName = "Different EndLineOffset")] + public void Equals_DifferentValues_ReturnsFalse(string ruleKey, string filePath, int startLine, int startLineOffset, int endLine, int endLineOffset) + { + var diagnostic2 = CreateDiagnostic(ruleKey, new FileUri(filePath), startLine, startLineOffset, endLine, endLineOffset); + + var result = testSubject.Equals(diagnostic1, diagnostic2); + + result.Should().BeFalse(); + } + + [TestMethod] + public void GetHashCode_SameObjects_ReturnsSameHashCode() + { + var diagnostic2 = CreateDiagnostic("rule1", new FileUri("file:///a/file1.cs"), 1, 1, 1, 10); + + var hash1 = testSubject.GetHashCode(diagnostic1); + var hash2 = testSubject.GetHashCode(diagnostic2); + + hash1.Should().Be(hash2); + } + + [TestMethod] + public void GetHashCode_DifferentObjects_ReturnsDifferentHashCodes() + { + var diagnostic2 = CreateDiagnostic("rule2", new FileUri("file:///a/file2.cs"), 2, 2, 2, 20); + + var hash1 = testSubject.GetHashCode(diagnostic1); + var hash2 = testSubject.GetHashCode(diagnostic2); + + hash1.Should().NotBe(hash2); + } + + [TestMethod] + public void Instance_ReturnsSingletonInstance() + { + var instance1 = DiagnosticDuplicatesComparer.Instance; + var instance2 = DiagnosticDuplicatesComparer.Instance; + + instance1.Should().BeSameAs(instance2); + } + + private static RoslynIssue CreateDiagnostic(string ruleKey, FileUri fileUri, int startLine, int startLineOffset, int endLine, int endLineOffset, string? message = null) + { + var textRange = new RoslynIssueTextRange(startLine, endLine, startLineOffset, endLineOffset); + var location = new RoslynIssueLocation(message ?? "message", fileUri, textRange); + return new RoslynIssue(ruleKey, location); + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticToRoslynIssueConverterTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticToRoslynIssueConverterTests.cs new file mode 100644 index 0000000000..40f143f2e6 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticToRoslynIssueConverterTests.cs @@ -0,0 +1,189 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using SonarLint.VisualStudio.Core.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.SLCore.Common.Models; +using SonarLint.VisualStudio.TestInfrastructure; +using Language = SonarLint.VisualStudio.Core.Language; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis; + +[TestClass] +public class DiagnosticToRoslynIssueConverterTests +{ + private static readonly ImmutableDictionary SecondaryLocationMessages = + new Dictionary { { "0", "First secondary message" }, { "1", "Second secondary message" } }.ToImmutableDictionary(); + private static readonly ImmutableDictionary? NoSecondaryLocationMessages = null; + + private readonly DiagnosticToRoslynIssueConverter testSubject = new(); + + [TestMethod] + public void MefCtor_CheckExports() => MefTestHelpers.CheckTypeCanBeImported(); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + public static object[][] TestData => + [ + [Language.CSharp, "S1234", "test message", "c:\\test\\file.cs", 0, 0, 3, 4], + [Language.CSharp, "S1234", "test message", "c:\\test\\file.cs", 1, 1, 3, 4], + [Language.CSharp, "S5678", "multi-line issue", "c:\\test\\file2.cs", 5, 10, 15, 20], + [Language.VBNET, "S1234", "test message", "c:\\test\\file.vb", 0, 0, 3, 4] + ]; + + [DataTestMethod] + [DynamicData(nameof(TestData))] + public void ConvertToSonarDiagnostic_ConvertsDiagnosticCorrectly( + Language language, + string ruleId, + string message, + string fileName, + int startLine, + int endLine, + int startChar, + int endChar) + { + var fileUri = new FileUri(fileName); + var location = CreateLocation(fileUri, startLine, endLine, startChar, endChar); + var diagnostic = CreateDiagnostic(ruleId, message, location); + var expectedTextRange = new RoslynIssueTextRange( + startLine + 1, // Convert to 1-based + endLine + 1, // Convert to 1-based + startChar, + endChar); + var expectedLocation = new RoslynIssueLocation( + message, + fileUri, + expectedTextRange); + var expectedRuleId = $"{language.RepoInfo.Key}:{ruleId}"; + var expectedDiagnostic = new RoslynIssue( + expectedRuleId, + expectedLocation); + + var result = testSubject.ConvertToSonarDiagnostic(diagnostic, [], language); + + result.Should().BeEquivalentTo(expectedDiagnostic); + } + + public static object?[][] SecondaryLocationTestData => + [ + [ + SecondaryLocationMessages, + SecondaryLocationMessages.OrderBy(x => x.Key).Select(x => x.Value).ToArray() + ], + [ + NoSecondaryLocationMessages, + new[] { "Location 0", "Location 1" } + ] + ]; + + [DataTestMethod] + [DynamicData(nameof(SecondaryLocationTestData))] + public void ConvertToSonarDiagnostic_WithSecondaryLocations_ConvertsCorrectly( + ImmutableDictionary? properties, + string[] expectedMessages) + { + const string fileCs = "c:\\test\\file.cs"; + const string file2Cs = "c:\\test\\file2.cs"; + var primaryLocation = CreateLocation(new FileUri(fileCs), 5, 5, 10, 15); + var additionalLocations = new[] { CreateLocation(new FileUri(fileCs), 10, 10, 20, 25), CreateLocation(new FileUri(file2Cs), 15, 15, 30, 35) }; + var diagnostic = CreateDiagnostic("any", "any", primaryLocation, additionalLocations, properties); + var expectedFlows = new[] + { + new RoslynIssueFlow(new List + { + new( + expectedMessages[0], + new FileUri(fileCs), + new RoslynIssueTextRange(11, 11, 20, 25)), + new( + expectedMessages[1], + new FileUri(file2Cs), + new RoslynIssueTextRange(16, 16, 30, 35)) + }) + }; + + var result = testSubject.ConvertToSonarDiagnostic(diagnostic, [], Language.CSharp); + + result.Flows.Should().BeEquivalentTo(expectedFlows); + } + + [TestMethod] + public void ConvertToSonarDiagnostic_WithQuickFixes_ConvertsCorrectly() + { + var diagnostic = CreateDiagnostic("any", "any", CreateLocation(new FileUri("file:///C:/any.cs"), 0, 0, 0, 0)); + var quickFix1 = new RoslynQuickFix(Guid.NewGuid()); + var quickFix2 = new RoslynQuickFix(Guid.NewGuid()); + + var result = testSubject.ConvertToSonarDiagnostic(diagnostic, [quickFix1, quickFix2], Language.CSharp); + + result.QuickFixes.Should().BeEquivalentTo([new RoslynIssueQuickFix(quickFix1.GetStorageValue()), new RoslynIssueQuickFix(quickFix2.GetStorageValue())], + options => options.ComparingByMembers()); + } + + [TestMethod] + public void ConvertToSonarDiagnostic_WithNoQuickFixes_ReturnsEmptyQuickFixesList() + { + var diagnostic = CreateDiagnostic("any", "any", CreateLocation(new FileUri("file:///C:/any.cs"), 0, 0, 0, 0)); + + var result = testSubject.ConvertToSonarDiagnostic(diagnostic, [], Language.CSharp); + + result.QuickFixes.Should().BeEmpty(); + } + + private static Location CreateLocation( + FileUri fileUri, + int startLine, + int endLine, + int startChar, + int endChar) + { + var textSpan = new TextSpan(12, 34); + var syntaxTree = Substitute.For(); + var linePositionSpan = new LinePositionSpan( + new LinePosition(startLine, startChar), + new LinePosition(endLine, endChar)); + syntaxTree.GetMappedLineSpan(textSpan, CancellationToken.None).Returns(new FileLinePositionSpan(fileUri.LocalPath, linePositionSpan)); + + return Location.Create(syntaxTree, textSpan); + } + + private static Diagnostic CreateDiagnostic( + string id, + string message, + Location location, + Location[]? additionalLocations = null, + ImmutableDictionary? properties = null) + { + var descriptor = new DiagnosticDescriptor( + id, + "Any Title", + message, + "Any Category", + default, + default); + + return Diagnostic.Create(descriptor, location, additionalLocations: additionalLocations, properties: properties ?? ImmutableDictionary.Empty); + } +} diff --git a/src/ConnectedMode.UnitTests/ServerSentEvents/QualityProfile/QualityProfileServerEventChannelTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynCodeActionFactoryTests.cs similarity index 66% rename from src/ConnectedMode.UnitTests/ServerSentEvents/QualityProfile/QualityProfileServerEventChannelTests.cs rename to src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynCodeActionFactoryTests.cs index 16ebaf9cd6..328b16ce01 100644 --- a/src/ConnectedMode.UnitTests/ServerSentEvents/QualityProfile/QualityProfileServerEventChannelTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynCodeActionFactoryTests.cs @@ -18,17 +18,17 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.QualityProfile; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; using SonarLint.VisualStudio.TestInfrastructure; -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.ServerSentEvents.QualityProfile; +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis; [TestClass] -public class QualityProfileServerEventChannelTests +public class RoslynCodeActionFactoryTests { [TestMethod] - public void MefCtor_CheckExportsMultipleInterfacesButSingleton() - { - MefTestHelpers.CheckMultipleExportsReturnSameInstance(); - } + public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported(); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); } diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynFileSemanticAnalysisTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynFileSemanticAnalysisTests.cs new file mode 100644 index 0000000000..7e07267482 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynFileSemanticAnalysisTests.cs @@ -0,0 +1,106 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using NSubstitute.ReturnsExtensions; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis; + +[TestClass] +public class RoslynFileSemanticAnalysisTests +{ + private const string TestFilePath = "c:\\test\\file.cs"; + private IRoslynCompilationWithAnalyzersWrapper compilationWrapper = null!; + private TestLogger testLogger = null!; + private RoslynFileSemanticAnalysis testSubject = null!; + + [TestInitialize] + public void TestInitialize() + { + testLogger = Substitute.ForPartsOf(); + compilationWrapper = Substitute.For(); + + testSubject = new RoslynFileSemanticAnalysis(TestFilePath, testLogger); + } + + [TestMethod] + public void MefCtor() => + testSubject.AnalysisFilePath.Should().Be(TestFilePath); + + [TestMethod] + public async Task ExecuteAsync_SemanticModelIsNull_ReturnsEmptyCollection() + { + compilationWrapper.GetSemanticModel(TestFilePath).ReturnsNull(); + + var result = await testSubject.ExecuteAsync(compilationWrapper, CancellationToken.None); + + result.Should().NotBeNull(); + result.Should().BeEmpty(); + } + + [TestMethod] + public async Task ExecuteAsync_SemanticModelExists_ReturnsAnalyzerDiagnostics() + { + var semanticModel = Substitute.For(); + var expectedDiagnostics = ImmutableArray.Create(CreateTestDiagnostic("id1"), CreateTestDiagnostic("id2"), CreateTestDiagnostic("id3")); + compilationWrapper.GetSemanticModel(TestFilePath).Returns(semanticModel); + compilationWrapper.GetAnalyzerSemanticDiagnosticsAsync(semanticModel, CancellationToken.None) + .Returns(expectedDiagnostics); + + var result = await testSubject.ExecuteAsync(compilationWrapper, CancellationToken.None); + + result.Should().BeEquivalentTo(expectedDiagnostics); + } + + [TestMethod] + public async Task ExecuteAsync_CancellationTokenPassed_UsesTokenForDiagnostics() + { + var semanticModel = Substitute.For(); + var cancellationToken = new CancellationToken(true); + compilationWrapper.GetSemanticModel(TestFilePath).Returns(semanticModel); + + await testSubject.ExecuteAsync(compilationWrapper, cancellationToken); + + await compilationWrapper.Received(1).GetAnalyzerSemanticDiagnosticsAsync(semanticModel, cancellationToken); + } + + private static Diagnostic CreateTestDiagnostic(string id) + { + var descriptor = new DiagnosticDescriptor( + id, + "title", + "message", + "category", + DiagnosticSeverity.Warning, + true); + + var location = Location.Create( + "test.cs", + new TextSpan(0, 1), + new LinePositionSpan(new LinePosition(0, 0), new LinePosition(0, 1))); + + return Diagnostic.Create(descriptor, location); + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynFileSyntaxAnalysisTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynFileSyntaxAnalysisTests.cs new file mode 100644 index 0000000000..15e87c98c1 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynFileSyntaxAnalysisTests.cs @@ -0,0 +1,107 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using NSubstitute.ReturnsExtensions; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis; + +[TestClass] +public class RoslynFileSyntaxAnalysisTests +{ + private const string TestFilePath = "test.cs"; + + private IRoslynCompilationWithAnalyzersWrapper compilationWrapper = null!; + private SyntaxTree syntaxTree = null!; + private TestLogger testLogger = null!; + private RoslynFileSyntaxAnalysis testSubject = null!; + + [TestInitialize] + public void TestInitialize() + { + testLogger = Substitute.ForPartsOf(); + compilationWrapper = Substitute.For(); + syntaxTree = Substitute.For(); + + testSubject = new RoslynFileSyntaxAnalysis(TestFilePath, testLogger); + } + + [TestMethod] + public void Constructor_SetsProperties() => + testSubject.AnalysisFilePath.Should().Be(TestFilePath); + + [TestMethod] + public async Task ExecuteAsync_SyntaxTreeExists_ReturnsSyntaxDiagnostics() + { + var expectedDiagnostics = ImmutableArray.Create(CreateTestDiagnostic("id1"), CreateTestDiagnostic("id2")); + compilationWrapper.GetSyntaxTree(TestFilePath).Returns(syntaxTree); + compilationWrapper.GetAnalyzerSyntaxDiagnosticsAsync(syntaxTree, Arg.Any()).Returns(expectedDiagnostics); + + var result = await testSubject.ExecuteAsync(compilationWrapper, CancellationToken.None); + + result.Should().BeEquivalentTo(expectedDiagnostics); + } + + [TestMethod] + public async Task ExecuteAsync_NoSyntaxTree_ReturnsEmptyArray() + { + compilationWrapper.GetSyntaxTree(TestFilePath).ReturnsNull(); + + var result = await testSubject.ExecuteAsync(compilationWrapper, CancellationToken.None); + + result.Should().BeEmpty(); + } + + [TestMethod] + public async Task ExecuteAsync_CancellationTokenPassed_UsesTokenForDiagnostics() + { + var expectedToken = new CancellationToken(true); + compilationWrapper.GetSyntaxTree(TestFilePath).Returns(syntaxTree); + + await testSubject.ExecuteAsync(compilationWrapper, expectedToken); + + await compilationWrapper.Received(1).GetAnalyzerSyntaxDiagnosticsAsync( + syntaxTree, + Arg.Is(token => token.Equals(expectedToken))); + } + + private static Diagnostic CreateTestDiagnostic(string id) + { + var descriptor = new DiagnosticDescriptor( + id, + "title", + "message", + "category", + DiagnosticSeverity.Warning, + true); + + var location = Location.Create( + "test.cs", + new TextSpan(0, 1), + new LinePositionSpan(new LinePosition(0, 0), new LinePosition(0, 1))); + + return Diagnostic.Create(descriptor, location); + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynProjectCompilationProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynProjectCompilationProviderTests.cs new file mode 100644 index 0000000000..acc9b2c5d0 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynProjectCompilationProviderTests.cs @@ -0,0 +1,195 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis; + +[TestClass] +public class RoslynProjectCompilationProviderTests +{ + private DiagnosticAnalyzer analyzer1 = null!; + private DiagnosticAnalyzer analyzer2 = null!; + private DiagnosticAnalyzer analyzer3 = null!; + private AnalyzerOptions analyzerOptions = null!; + private ImmutableArray analyzers; + private ImmutableDictionary> codeFixProviders = null!; + private IRoslynCompilationWrapper compilation = null!; + private CompilationOptions compilationOptions = null!; + private IRoslynCompilationWithAnalyzersWrapper compilationWithAnalyzers = null!; + private ImmutableDictionary configurations = null!; + private ImmutableDictionary diagnosticOptions = null!; + private AdditionalText existingAdditionalFile = null!; + private TestLogger logger = null!; + private IRoslynProjectWrapper project = null!; + private SonarLintXmlConfigurationFile sonarLintXml = null!; + private RoslynProjectCompilationProvider testSubject = null!; + + [TestInitialize] + public void TestInitialize() + { + logger = Substitute.ForPartsOf(); + + SetUpCompilation(); + SetUpAdditionalFiles(); + SetUpProject(); + SetUpAnalyzers(); + SetUpCodeFixProviders(); + diagnosticOptions = ImmutableDictionary.Empty + .Add("SomeId", ReportDiagnostic.Warn); + configurations = ImmutableDictionary.Empty + .Add(Language.CSharp, new RoslynAnalysisConfiguration( + sonarLintXml, + diagnosticOptions, + analyzers, + codeFixProviders)); + SetUpCompilationWithAnalyzers(); + testSubject = new RoslynProjectCompilationProvider(logger); + } + + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public void Ctor_SetsLogContext() => + logger.Received(1).ForContext(Resources.RoslynLogContext, Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisAnalyzerExceptionLogContext); + + [TestMethod] + public async Task GetProjectCompilationAsync_ConfiguresCompilationWithCorrectOptions() + { + var result = await testSubject.GetProjectCompilationAsync(project, configurations, CancellationToken.None); + + result.Should().Be(compilationWithAnalyzers); + compilation.Received(1).WithOptions(Arg.Is(options => + options.SpecificDiagnosticOptions == diagnosticOptions)); + compilation.Received(1).WithAnalyzers( + Arg.Is>(analyzersArg => + analyzersArg.SequenceEqual(analyzers, null as IEqualityComparer)), + Arg.Is(options => + options.Options != null + && options.Options.AdditionalFiles.SequenceEqual(ImmutableArray.Create(existingAdditionalFile, sonarLintXml), null as IEqualityComparer) + && options.ConcurrentAnalysis == true + && options.ReportSuppressedDiagnostics == false + && options.LogAnalyzerExecutionTime == false), + configurations[Language.CSharp]); + } + + [TestMethod] + public async Task GetProjectCompilationAsync_RemovesExistingSonarLintXml() + { + var existingSonarLintXml = Substitute.For(); + existingSonarLintXml.Path.Returns(@"c:\some\other\path\SonarLint.xml"); + var analyzerOptionsWithSonarLintXml = new AnalyzerOptions( + ImmutableArray.Create(existingAdditionalFile, existingSonarLintXml)); + project.RoslynAnalyzerOptions.Returns(analyzerOptionsWithSonarLintXml); + compilation.WithAnalyzers(Arg.Any>(), Arg.Any(), configurations[Language.CSharp]) + .Returns(compilationWithAnalyzers); + + await testSubject.GetProjectCompilationAsync(project, configurations, CancellationToken.None); + + compilation.Received(1).WithAnalyzers( + Arg.Any>(), + Arg.Is(options => + options.Options != null + && options.Options.AdditionalFiles.SequenceEqual(ImmutableArray.Create(existingAdditionalFile, sonarLintXml), null as IEqualityComparer) + && options.ConcurrentAnalysis == true + && options.ReportSuppressedDiagnostics == false + && options.LogAnalyzerExecutionTime == false), + configurations[Language.CSharp]); + } + + [TestMethod] + public async Task GetProjectCompilationAsync_AnalyzerException_LogsError() + { + CompilationWithAnalyzersOptions capturedOptions = null!; + compilation.WithAnalyzers( + Arg.Any>(), + Arg.Do(x => capturedOptions = x), configurations[Language.CSharp]) + .Returns(compilationWithAnalyzers); + await testSubject.GetProjectCompilationAsync(project, configurations, CancellationToken.None); + capturedOptions.Should().NotBeNull(); + var exception = new InvalidOperationException("test exception"); + var diagnostic = Diagnostic.Create("TestId", "TestCategory", "TestMessage", DiagnosticSeverity.Warning, DiagnosticSeverity.Warning, true, 1); + + capturedOptions.OnAnalyzerException!(exception, analyzer1, diagnostic); + + logger.AssertPartialOutputStringExists( + analyzer1.GetType().Name, + "TestId", + "test exception"); + } + + private void SetUpAnalyzers() + { + analyzer1 = Substitute.For(); + analyzer2 = Substitute.For(); + analyzer3 = Substitute.For(); + analyzers = ImmutableArray.Create(analyzer1, analyzer2, analyzer3); + } + + private void SetUpCodeFixProviders() + { + codeFixProviders = ImmutableDictionary>.Empty.Add("1", [Substitute.For()]); + } + + private void SetUpProject() + { + project = Substitute.For(); + analyzerOptions = new AnalyzerOptions(ImmutableArray.Create(existingAdditionalFile)); + project.RoslynAnalyzerOptions.Returns(analyzerOptions); + project.GetCompilationAsync(Arg.Any()).Returns(compilation); + } + + private void SetUpAdditionalFiles() + { + sonarLintXml = new SonarLintXmlConfigurationFile(@"C:\B\A", "content"); + + existingAdditionalFile = Substitute.For(); + existingAdditionalFile.Path.Returns(@"c:\path\to\existing.txt"); + } + + private void SetUpCompilation() + { + compilation = Substitute.For(); + compilationOptions = new CSharpCompilationOptions(OutputKind.ConsoleApplication); + compilation.RoslynCompilationOptions.Returns(compilationOptions); + compilationWithAnalyzers = Substitute.For(); + compilation.Language.Returns(Language.CSharp); + compilation.WithOptions(Arg.Any()).Returns(compilation); + } + + private void SetUpCompilationWithAnalyzers() => + compilation.WithAnalyzers(Arg.Any>(), Arg.Any(), configurations[Language.CSharp]) + .Returns(compilationWithAnalyzers); +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynQuickFixFactoryTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynQuickFixFactoryTests.cs new file mode 100644 index 0000000000..011d49a81a --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynQuickFixFactoryTests.cs @@ -0,0 +1,154 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Text; +using NSubstitute.ReturnsExtensions; +using SonarLint.VisualStudio.Integration.TestInfrastructure; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis; + +[TestClass] +public class RoslynQuickFixFactoryTests +{ + private IRoslynWorkspaceWrapper workspaceWrapper = null!; + private IRoslynCodeActionFactory roslynCodeActionFactory = null!; + private IRoslynQuickFixStorageWriter quickFixStorage = null!; + private IRoslynSolutionWrapper solution = null!; + private Diagnostic diagnostic = null!; + private IRoslynDocumentWrapper document = null!; + private IReadOnlyCollection codeFixProviders = null!; + private CancellationToken token; + private RoslynQuickFixFactory testSubject = null!; + + [TestInitialize] + public void TestInitialize() + { + codeFixProviders = [Substitute.For(), Substitute.For()]; + document = Substitute.For(); + diagnostic = CreateDiagnostic("rule1"); + workspaceWrapper = Substitute.For(); + roslynCodeActionFactory = Substitute.For(); + quickFixStorage = Substitute.For(); + solution = Substitute.For(); + token = CancellationToken.None; + + testSubject = new RoslynQuickFixFactory(workspaceWrapper, roslynCodeActionFactory, quickFixStorage); + } + + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public async Task CreateQuickFixesAsync_NoCodeFixProvidersForRule_ReturnsEmptyList() + { + var result = await testSubject.CreateQuickFixesAsync(diagnostic, solution, CreateAnalysisConfiguration([]), token); + + result.Should().BeEmpty(); + roslynCodeActionFactory.DidNotReceiveWithAnyArgs().GetCodeActionsAsync(default!, default!, default!, default).IgnoreAwaitForAssert(); + quickFixStorage.DidNotReceiveWithAnyArgs().Add(default, default!); + } + + [TestMethod] + public async Task CreateQuickFixesAsync_NoDocumentFound_ReturnsEmptyList() + { + var analysisConfiguration = CreateAnalysisConfiguration(new() { { diagnostic.Id, codeFixProviders } }); + solution.GetDocument(diagnostic.Location.SourceTree).ReturnsNull(); + + var result = await testSubject.CreateQuickFixesAsync(diagnostic, solution, analysisConfiguration, token); + + result.Should().BeEmpty(); + roslynCodeActionFactory.DidNotReceiveWithAnyArgs().GetCodeActionsAsync(default!, default!, default!, default).IgnoreAwaitForAssert(); + quickFixStorage.DidNotReceiveWithAnyArgs().Add(default, default!); + } + + [TestMethod] + public async Task CreateQuickFixesAsync_NoCodeActionsFound_ReturnsEmptyList() + { + var analysisConfiguration = CreateAnalysisConfiguration(new() { { diagnostic.Id, codeFixProviders } }); + solution.GetDocument(diagnostic.Location.SourceTree).Returns(document); + roslynCodeActionFactory.GetCodeActionsAsync(default!, default!, default!, default) + .ReturnsForAnyArgs([]); + + var result = await testSubject.CreateQuickFixesAsync(diagnostic, solution, analysisConfiguration, token); + + result.Should().BeEmpty(); + roslynCodeActionFactory.Received(1).GetCodeActionsAsync(codeFixProviders, diagnostic, document, token).IgnoreAwaitForAssert(); + quickFixStorage.DidNotReceiveWithAnyArgs().Add(default, default!); + } + + [TestMethod] + public async Task CreateQuickFixesAsync_WithCodeActions_ReturnsQuickFixesAndAddsToStorage() + { + var codeAction1 = Substitute.For(); + var codeAction2 = Substitute.For(); + var codeAction3 = Substitute.For(); + var analysisConfiguration = CreateAnalysisConfiguration(new() { { diagnostic.Id, codeFixProviders } }); + solution.GetDocument(diagnostic.Location.SourceTree).Returns(document); + roslynCodeActionFactory.GetCodeActionsAsync(Arg.Any>(), diagnostic, document, token) + .Returns([codeAction1, codeAction2, codeAction3]); + + var result = await testSubject.CreateQuickFixesAsync(diagnostic, solution, analysisConfiguration, token); + + result.Should().HaveCount(3); + roslynCodeActionFactory.Received(1).GetCodeActionsAsync(codeFixProviders, diagnostic, document, token).IgnoreAwaitForAssert(); + quickFixStorage.Received(1).Add(result[0].Id, Arg.Is(x => x.RoslynCodeAction == codeAction1)); + quickFixStorage.Received(1).Add(result[1].Id, Arg.Is(x => x.RoslynCodeAction == codeAction2)); + quickFixStorage.Received(1).Add(result[2].Id, Arg.Is(x => x.RoslynCodeAction == codeAction3)); + } + + private static Diagnostic CreateDiagnostic(string id) + { + var descriptor = new DiagnosticDescriptor( + id, + "any", + "any", + "any", + DiagnosticSeverity.Warning, + true); + + var location = Location.Create( + "any", + new TextSpan(0, 1), + new LinePositionSpan(new LinePosition(0, 0), new LinePosition(0, 1))); + + return Diagnostic.Create(descriptor, location); + } + + private static RoslynAnalysisConfiguration CreateAnalysisConfiguration( + Dictionary> codeFixProvidersByRuleKey) => + new(null!, + null!, + default, + codeFixProvidersByRuleKey.ToImmutableDictionary()); +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynSolutionAnalysisCommandProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynSolutionAnalysisCommandProviderTests.cs new file mode 100644 index 0000000000..08a537f98b --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynSolutionAnalysisCommandProviderTests.cs @@ -0,0 +1,201 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis; + +[TestClass] +public class RoslynSolutionAnalysisCommandProviderTests +{ + private const string File1Cs = "file1.cs"; + private const string File2Cs = "file2.cs"; + private const string File3Cs = "file3.cs"; + private const string File4Cs = "file4.cs"; + private const string AnalyzedFile1Cs = "analyzedFile1.cs"; + private const string AnalyzedFile2Cs = "analyzedFile2.cs"; + private const string AnalyzedFile3Cs = "analyzedFile3.cs"; + private const string Project1 = "project1"; + private const string Project2 = "project2"; + private const string Project3 = "project3"; + private const string Project4 = "project4"; + + private IRoslynWorkspaceWrapper workspaceWrapper = null!; + private TestLogger logger = null!; + private IRoslynSolutionWrapper solutionWrapper = null!; + private RoslynSolutionAnalysisCommandProvider testSubject = null!; + + [TestInitialize] + public void TestInitialize() + { + workspaceWrapper = Substitute.For(); + logger = Substitute.ForPartsOf(); + solutionWrapper = Substitute.For(); + workspaceWrapper.GetCurrentSolution().Returns(solutionWrapper); + testSubject = new RoslynSolutionAnalysisCommandProvider(workspaceWrapper, logger); + } + + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => + MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public void Ctor_SetsLogContext() => + logger.Received(1).ForContext(Resources.RoslynLogContext, Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisConfigurationLogContext); + + [TestMethod] + public void GetAnalysisCommandsForCurrentSolution_NoProjects_ReturnsEmptyList() + { + solutionWrapper.Projects.Returns(new List()); + + var result = testSubject.GetAnalysisCommandsForCurrentSolution([File1Cs]); + + result.Should().BeEmpty(); + logger.AssertPartialOutputStringExists("No projects to analyze"); + } + + [TestMethod] + public void GetAnalysisCommandsForCurrentSolution_ProjectDoesNotSupportCompilation_SkipsProject() + { + var project1 = CreateProject(Project1, false); + solutionWrapper.Projects.Returns([project1]); + + var result = testSubject.GetAnalysisCommandsForCurrentSolution([File1Cs]); + + result.Should().BeEmpty(); + logger.AssertPartialOutputStringExists("Project project1 does not support compilation"); + logger.AssertPartialOutputStringExists("No projects to analyze"); + } + + [TestMethod] + public void GetAnalysisCommandsForCurrentSolution_ProjectWithNoMatchingFiles_SkipsProject() + { + var project1 = CreateProject(Project1); + project1.ContainsDocument(Arg.Any(), out _).Returns(false); + + solutionWrapper.Projects.Returns([project1]); + + var result = testSubject.GetAnalysisCommandsForCurrentSolution([File1Cs]); + + result.Should().BeEmpty(); + logger.AssertPartialOutputStringExists("No projects to analyze"); + } + + [TestMethod] + public void GetAnalysisCommandsForCurrentSolution_MultipleProjects_OnlyProjectWithFilesIsReturned() + { + var project1 = CreateProject(Project1); + project1.ContainsDocument(Arg.Any(), out _).Returns(false); + var project2 = CreateProject(Project2); + SetupContainsDocument(project2, File1Cs, AnalyzedFile1Cs); + solutionWrapper.Projects.Returns([project1, project2]); + + var result = testSubject.GetAnalysisCommandsForCurrentSolution([File1Cs]); + + result.Should().ContainSingle(); + var command = result.Single(); + command.Project.Should().Be(project2); + command.AnalysisCommands.OfType().Should().HaveCount(1); + command.AnalysisCommands.OfType().Should().HaveCount(1); + } + + [TestMethod] + public void GetAnalysisCommandsForCurrentSolution_MultipleMatchingFiles_AllFilesIncluded() + { + var project = CreateProject(Project1); + SetupContainsDocument(project, File1Cs, AnalyzedFile1Cs); + SetupContainsDocument(project, File2Cs, AnalyzedFile2Cs); + project.ContainsDocument(File3Cs, out _).Returns(false); + solutionWrapper.Projects.Returns([project]); + + var result = testSubject.GetAnalysisCommandsForCurrentSolution([File1Cs, File2Cs, File3Cs]); + + var command = result.Single(); + command.Project.Should().Be(project); + command.AnalysisCommands.Should().HaveCount(4); + ValidateContainsAllTypesOfAnalysisForFile(command, AnalyzedFile1Cs); + ValidateContainsAllTypesOfAnalysisForFile(command, AnalyzedFile2Cs); + } + + [TestMethod] + public void GetAnalysisCommandsForCurrentSolution_MixedProjectResults_ReturnsCorrectProjects() + { + var projectWithNoCompilation = CreateProject(Project1, false); + var projectWithNofiles = CreateProject(Project2); + projectWithNofiles.ContainsDocument(Arg.Any(), out _).Returns(false); + var project3 = CreateProject(Project3); + SetupContainsDocument(project3, File1Cs, AnalyzedFile1Cs); + SetupContainsDocument(project3, File2Cs, AnalyzedFile2Cs); + var project4 = CreateProject(Project4); + SetupContainsDocument(project4, File3Cs, AnalyzedFile3Cs); + SetupContainsDocument(project4, File1Cs, AnalyzedFile1Cs); + solutionWrapper.Projects.Returns([projectWithNoCompilation, projectWithNofiles, project3, project4]); + + var result = testSubject.GetAnalysisCommandsForCurrentSolution([File1Cs, File2Cs, File3Cs, File4Cs]); + + result.Should().HaveCount(2); + result[0].Project.Should().Be(project3); + result[0].AnalysisCommands.Should().HaveCount(4); + ValidateContainsAllTypesOfAnalysisForFile(result[0], AnalyzedFile1Cs); + ValidateContainsAllTypesOfAnalysisForFile(result[0], AnalyzedFile2Cs); + result[1].Project.Should().Be(project4); + result[1].AnalysisCommands.Should().HaveCount(4); + ValidateContainsAllTypesOfAnalysisForFile(result[1], AnalyzedFile3Cs); + ValidateContainsAllTypesOfAnalysisForFile(result[1], AnalyzedFile1Cs); + logger.AssertPartialOutputStringExists("Project project1 does not support compilation"); + } + + private void ValidateContainsAllTypesOfAnalysisForFile(RoslynProjectAnalysisRequest request, string analysisFilePath) + { + ValidateContainsSyntacticAnalysisForFile(request, analysisFilePath); + ValidateContainsSemanticAnalysisForFile(request, analysisFilePath); + } + + private void ValidateContainsSyntacticAnalysisForFile(RoslynProjectAnalysisRequest request, string analysisFilePath) => + request.AnalysisCommands.Any(x => x is RoslynFileSyntaxAnalysis semanticAnalysis && semanticAnalysis.AnalysisFilePath == analysisFilePath).Should().BeTrue(); + + private void ValidateContainsSemanticAnalysisForFile(RoslynProjectAnalysisRequest request, string analysisFilePath) => + request.AnalysisCommands.Any(x => x is RoslynFileSemanticAnalysis semanticAnalysis && semanticAnalysis.AnalysisFilePath == analysisFilePath).Should().BeTrue(); + + + private static IRoslynProjectWrapper CreateProject(string projectName, bool supportsCompilation = true) + { + var project = Substitute.For(); + project.Name.Returns(projectName); + project.SupportsCompilation.Returns(supportsCompilation); + return project; + } + + private static void SetupContainsDocument(IRoslynProjectWrapper project, string file, string analyzedFile) => + project.ContainsDocument(file, out Arg.Any()).Returns(x => + { + x[1] = analyzedFile; + return true; + }); +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/SequentialRoslynAnalysisEngineTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/SequentialRoslynAnalysisEngineTests.cs new file mode 100644 index 0000000000..63a06e9580 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/SequentialRoslynAnalysisEngineTests.cs @@ -0,0 +1,331 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Analysis; +using SonarLint.VisualStudio.Integration.TestInfrastructure; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; +using SonarLint.VisualStudio.SLCore.Common.Models; +using SonarLint.VisualStudio.TestInfrastructure; +using Language = SonarLint.VisualStudio.Core.Language; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis; + +[TestClass] +public class SequentialRoslynAnalysisEngineTests +{ + private IDiagnosticToRoslynIssueConverter issueConverter = null!; + private IRoslynProjectCompilationProvider projectCompilationProvider = null!; + private TestLogger logger = null!; + private ImmutableDictionary configurations = null!; + private CancellationToken cancellationToken; + private SequentialRoslynAnalysisEngine testSubject = null!; + private IRoslynQuickFixFactory roslynQuickFixFactory = null!; + private IRoslynSolutionWrapper solution = null!; + + [TestInitialize] + public void TestInitialize() + { + issueConverter = Substitute.For(); + projectCompilationProvider = Substitute.For(); + logger = Substitute.ForPartsOf(); + roslynQuickFixFactory = Substitute.For(); + roslynQuickFixFactory.CreateQuickFixesAsync(default!, default!, default, default).ReturnsForAnyArgs([]); + solution = Substitute.For(); + + testSubject = new SequentialRoslynAnalysisEngine(issueConverter, projectCompilationProvider, roslynQuickFixFactory, logger); + + configurations = ImmutableDictionary.Create(); + cancellationToken = new CancellationToken(); + } + + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public void Ctor_SetsLogContext() => + logger.Received(1).ForContext(Resources.RoslynLogContext, Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisEngineLogContext); + + [TestMethod] + public async Task AnalyzeAsync_EmptyAnalysisCommands_ReturnsEmptyCollection() + { + var result = await testSubject.AnalyzeAsync([], configurations, cancellationToken); + + result.Should().BeEmpty(); + } + + [TestMethod] + public async Task AnalyzeAsync_SingleProjectWithNoAnalysisCommands_ReturnsEmptyCollection() + { + var (project, _) = SetupProjectAnalysisRequestAndCompilation(); + + var result = await testSubject.AnalyzeAsync([CreateProjectRequest(project)], configurations, cancellationToken); + + result.Should().BeEmpty(); + await projectCompilationProvider.Received(1).GetProjectCompilationAsync(project, configurations, cancellationToken); + } + + public static object[][] RoslynLanguages => + [ + [Language.CSharp], + [Language.VBNET] + ]; + + [DataTestMethod] + [DynamicData(nameof(RoslynLanguages))] + public async Task AnalyzeAsync_SingleProjectWithSingleAnalysisCommand_ReturnsDiagnostics(Language language) + { + var (diagnostic, roslynIssue) = SetUpDiagnosticAndConvertedModel("test-rule", "test message"); + var (requestForProject, compilationForProject) = SetupProjectAnalysisRequestAndCompilation([[diagnostic]]); + compilationForProject.Language.Returns(language); + + var result = await testSubject.AnalyzeAsync([requestForProject], configurations, cancellationToken); + + result.Should().BeEquivalentTo(roslynIssue); + VerifyAnalysisExecution(requestForProject, compilationForProject, [diagnostic], language); + } + + [TestMethod] + public async Task AnalyzeAsync_DuplicateDiagnostics_ReturnsSingleDiagnostic() + { + var (duplicateDiagnostic1, duplicateIssue1) = SetUpDiagnosticAndConvertedModel("test-rule", "test message"); + var (duplicateDiagnostic2, duplicateIssue2) = SetUpDiagnosticAndConvertedModel("test-rule", "test message duplicate"); + var (requestForProject, compilationForProject) = SetupProjectAnalysisRequestAndCompilation([[duplicateDiagnostic1], [duplicateDiagnostic2]]); + + var result = await testSubject.AnalyzeAsync([requestForProject], configurations, cancellationToken); + + result.Should().BeEquivalentTo(duplicateIssue1); + VerifyAnalysisExecution(requestForProject, compilationForProject, [duplicateDiagnostic1, duplicateDiagnostic2]); + logger.AssertPartialOutputStringExists( + $"Duplicate diagnostic discarded ID: {duplicateIssue2.RuleId}, File: {duplicateIssue2.PrimaryLocation.FileUri.LocalPath}, Line: {duplicateIssue2.PrimaryLocation.TextRange.StartLine}"); + } + + [TestMethod] + public async Task AnalyzeAsync_DuplicateDiagnosticsInDifferentProjects_ReturnsSingleDiagnostic() + { + var (diagnostic1, duplicateIssue) = SetUpDiagnosticAndConvertedModel("test-rule", "test message"); + var (requestForProject1, compilationForProject1) = SetupProjectAnalysisRequestAndCompilation([[diagnostic1]]); + var (diagnostic2, _) = SetUpDiagnosticAndConvertedModel("test-rule-duplicate", "test message duplicate", duplicateIssue); + var (requestForProject2, compilationForProject2) = SetupProjectAnalysisRequestAndCompilation([[diagnostic2]]); + + var result = await testSubject.AnalyzeAsync([requestForProject1, requestForProject2], configurations, cancellationToken); + + result.Should().BeEquivalentTo(duplicateIssue); + VerifyAnalysisExecution(requestForProject1, compilationForProject1, [diagnostic1]); + VerifyAnalysisExecution(requestForProject2, compilationForProject2, [diagnostic2]); + logger.AssertPartialOutputStringExists( + $"Duplicate diagnostic discarded ID: {duplicateIssue.RuleId}, File: {duplicateIssue.PrimaryLocation.FileUri.LocalPath}, Line: {duplicateIssue.PrimaryLocation.TextRange.StartLine}"); + } + + [TestMethod] + public async Task AnalyzeAsync_MultipleProjects_ProcessesAllProjects() + { + var (diagnostic1, sonarIssue1) = SetUpDiagnosticAndConvertedModel("rule1", "message1"); + var (requestForProject1, compilationForProject1) = SetupProjectAnalysisRequestAndCompilation([[diagnostic1]]); + var (diagnostic2, sonarIssue2) = SetUpDiagnosticAndConvertedModel("rule2", "message2"); + var (requestForProject2, compilationForProject2) = SetupProjectAnalysisRequestAndCompilation([[diagnostic2]]); + + var result = await testSubject.AnalyzeAsync([requestForProject1, requestForProject2], configurations, cancellationToken); + + result.Should().BeEquivalentTo([sonarIssue1, sonarIssue2]); + VerifyAnalysisExecution(requestForProject1, compilationForProject1, [diagnostic1]); + VerifyAnalysisExecution(requestForProject2, compilationForProject2, [diagnostic2]); + } + + [TestMethod] + public async Task AnalyzeAsync_SingleProjectWithMultipleCommands_ReturnsAllDiagnostics() + { + var (diagnostic1A, sonarIssue1A) = SetUpDiagnosticAndConvertedModel("rule1", "message1"); + var (diagnostic1B, sonarIssue1B) = SetUpDiagnosticAndConvertedModel("rule2", "message2"); + var (diagnostic2A, sonarIssue2A) = SetUpDiagnosticAndConvertedModel("rule3", "message3"); + var (diagnostic2B, sonarIssue2B) = SetUpDiagnosticAndConvertedModel("rule4", "message4"); + var (requestForProject, compilationForProject) = SetupProjectAnalysisRequestAndCompilation([[diagnostic1A, diagnostic1B], [diagnostic2A, diagnostic2B]]); + + var result = await testSubject.AnalyzeAsync([requestForProject], configurations, cancellationToken); + + result.Should().BeEquivalentTo(sonarIssue1A, sonarIssue1B, sonarIssue2A, sonarIssue2B); + VerifyAnalysisExecution(requestForProject, compilationForProject, [diagnostic1A, diagnostic1B, diagnostic2A, diagnostic2B]); + } + + [TestMethod] + public async Task AnalyzeAsync_WithMultipleDiagnostics_CreatesQuickFixesForEach() + { + var analysisConfiguration1 = new RoslynAnalysisConfiguration(); + var noQuickFixes = new List(); + var (diagnosticWith0Fixes, sonarIssueWith0Fixes) = SetupDiagnosticWithQuickFixes("rule1", "message1", noQuickFixes, analysisConfiguration1); + var oneQuickFix = new List { new(Guid.NewGuid()) }; + var (diagnosticWith1Fix, sonarIssueWith1Fix) = SetupDiagnosticWithQuickFixes("rule2", "message2", oneQuickFix, analysisConfiguration1); + + var project2Configuration = new RoslynAnalysisConfiguration(); + var twoQuickFixes = new List { new(Guid.NewGuid()), new(Guid.NewGuid()) }; + var (diagnostic2FixesProject2, sonarIssue2FixesProject2) = SetupDiagnosticWithQuickFixes("rule3", "message3", twoQuickFixes, project2Configuration); + + var (requestForProject1, compilationForProject1) = SetupProjectAnalysisRequestAndCompilation([[diagnosticWith0Fixes], [diagnosticWith1Fix]], analysisConfiguration1); + var (requestForProject2, compilationForProject2) = SetupProjectAnalysisRequestAndCompilation([[diagnostic2FixesProject2]], project2Configuration); + + var result = await testSubject.AnalyzeAsync([requestForProject1, requestForProject2], configurations, cancellationToken); + + result.Should().BeEquivalentTo([sonarIssueWith0Fixes, sonarIssueWith1Fix, sonarIssue2FixesProject2], options => options.Excluding(x => x.QuickFixes)); // factory tests will be used to verify the quickfixes + VerifyAnalysisExecution(requestForProject1, compilationForProject1, [(diagnosticWith0Fixes, noQuickFixes), (diagnosticWith1Fix, oneQuickFix)]); + VerifyAnalysisExecution(requestForProject2, compilationForProject2, [(diagnostic2FixesProject2, twoQuickFixes)]); + } + + private (Diagnostic diagnostic, RoslynIssue sonarIssue) SetupDiagnosticWithQuickFixes( + string ruleId, + string message, + List quickFixes, + RoslynAnalysisConfiguration analysisConfiguration) + { + var (diagnostic, sonarIssue) = SetUpDiagnosticAndConvertedModel(ruleId, message, null, quickFixes); + + roslynQuickFixFactory.CreateQuickFixesAsync( + diagnostic, + solution, + analysisConfiguration, + cancellationToken) + .Returns(quickFixes); + + return (diagnostic, sonarIssue); + } + + private (RoslynProjectAnalysisRequest request, IRoslynCompilationWithAnalyzersWrapper compilation) SetupProjectAnalysisRequestAndCompilation( + Diagnostic[][] diagnosticsPerCommand, + RoslynAnalysisConfiguration? analysisConfiguration = null) + { + var (project, projectCompilation) = SetupProjectAnalysisRequestAndCompilation(analysisConfiguration); + var commands = diagnosticsPerCommand.Select(x => SetupCommandWithDiagnostics(projectCompilation, x)).ToArray(); + + return (new RoslynProjectAnalysisRequest(project, commands), projectCompilation); + } + + private RoslynProjectAnalysisRequest CreateProjectRequest(IRoslynProjectWrapper project, params IRoslynAnalysisCommand[] commands) => new(project, commands); + + private (IRoslynProjectWrapper project, IRoslynCompilationWithAnalyzersWrapper projectCompilation) SetupProjectAnalysisRequestAndCompilation( + RoslynAnalysisConfiguration? analysisConfiguration = null) + { + var project = Substitute.For(); + project.Solution.Returns(solution); + var compilation = SetupCompilation(project, analysisConfiguration ?? new RoslynAnalysisConfiguration()); + + return (project, compilation); + } + + private IRoslynAnalysisCommand SetupCommandWithDiagnostics( + IRoslynCompilationWithAnalyzersWrapper compilationWithAnalyzers, + params Diagnostic[] diagnostics) + { + var command = Substitute.For(); + command.ExecuteAsync(compilationWithAnalyzers, CancellationToken.None) + .Returns(ImmutableArray.Create(diagnostics)); + return command; + } + + private (Diagnostic, RoslynIssue) SetUpDiagnosticAndConvertedModel( + string ruleId, + string message, + RoslynIssue? existingSonarIssue = null, + List? roslynQuickFixes = null) + { + var diagnostic = CreateTestDiagnostic(ruleId, message); + + var sonarIssue = existingSonarIssue ?? CreateSonarIssue(ruleId, message); + issueConverter.ConvertToSonarDiagnostic(diagnostic, roslynQuickFixes ?? Arg.Any>(), Arg.Any()).Returns(sonarIssue); + + return (diagnostic, sonarIssue); + } + + private IRoslynCompilationWithAnalyzersWrapper SetupCompilation( + IRoslynProjectWrapper project, + RoslynAnalysisConfiguration analysisConfiguration) + { + var compilationWithAnalyzers = Substitute.For(); + compilationWithAnalyzers.AnalysisConfiguration.Returns(analysisConfiguration); + projectCompilationProvider.GetProjectCompilationAsync(project, configurations, cancellationToken) + .Returns(compilationWithAnalyzers); + return compilationWithAnalyzers; + } + + private void VerifyAnalysisExecution( + RoslynProjectAnalysisRequest projectRequest, + IRoslynCompilationWithAnalyzersWrapper compilationWithAnalyzers, + Diagnostic[] diagnostics, + Language? language = null) => + VerifyAnalysisExecution( + projectRequest, + compilationWithAnalyzers, + diagnostics.Select(x => (x, new List())).ToArray(), + language); + + private void VerifyAnalysisExecution( + RoslynProjectAnalysisRequest projectRequest, + IRoslynCompilationWithAnalyzersWrapper compilationWithAnalyzers, + (Diagnostic diagnostic, List quickFixes)[] diagnostics, + Language? language = null) + { + projectCompilationProvider.Received(1) + .GetProjectCompilationAsync(projectRequest.Project, configurations, cancellationToken).IgnoreAwaitForAssert(); + foreach (var analysisCommand in projectRequest.AnalysisCommands) + { + analysisCommand.Received(1).ExecuteAsync(compilationWithAnalyzers, cancellationToken).IgnoreAwaitForAssert(); + } + foreach (var (diagnostic, roslynQuickFixes) in diagnostics) + { + roslynQuickFixFactory.Received(1).CreateQuickFixesAsync(diagnostic, projectRequest.Project.Solution, compilationWithAnalyzers.AnalysisConfiguration, cancellationToken); + issueConverter.Received(1).ConvertToSonarDiagnostic(diagnostic, Arg.Is>(x => x.SequenceEqual(roslynQuickFixes)), language ?? Arg.Any()); + } + } + + private static Diagnostic CreateTestDiagnostic(string id, string message) + { + var descriptor = new DiagnosticDescriptor( + id, + "title", + message, + "category", + DiagnosticSeverity.Warning, + true); + + var location = Location.Create( + "C:\\test.cs", + new TextSpan(0, 1), + new LinePositionSpan(new LinePosition(0, 0), new LinePosition(0, 1))); + + return Diagnostic.Create(descriptor, location); + } + + private static RoslynIssue CreateSonarIssue(string ruleId, string message) + { + var textRange = new RoslynIssueTextRange(1, 1, 0, 1); + var location = new RoslynIssueLocation(message, new FileUri("C:\\test.cs"), textRange); + return new RoslynIssue(ruleId, location); + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Wrappers/RoslynWorkspaceWrapperTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Wrappers/RoslynWorkspaceWrapperTests.cs new file mode 100644 index 0000000000..fe31c6ab98 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Wrappers/RoslynWorkspaceWrapperTests.cs @@ -0,0 +1,32 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis.Wrappers; + +[TestClass] +public class RoslynWorkspaceWrapperTests +{ + [TestMethod] + public void MefCtor_CheckIsSingleton() => + MefTestHelpers.CheckIsSingletonMefComponent(); +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Wrappers/WorkspaceChangeIndicatorTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Wrappers/WorkspaceChangeIndicatorTests.cs new file mode 100644 index 0000000000..e1e9c8208d --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Wrappers/WorkspaceChangeIndicatorTests.cs @@ -0,0 +1,51 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.CodeAnalysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis.Wrappers; + +[TestClass] +public class WorkspaceChangeIndicatorTests +{ + [DataTestMethod] + [DataRow(WorkspaceChangeKind.DocumentAdded, true)] + [DataRow(WorkspaceChangeKind.DocumentRemoved, true)] + [DataRow(WorkspaceChangeKind.DocumentReloaded, true)] + [DataRow(WorkspaceChangeKind.DocumentChanged, true)] + [DataRow(WorkspaceChangeKind.DocumentInfoChanged, true)] + [DataRow(WorkspaceChangeKind.AdditionalDocumentAdded, true)] + [DataRow(WorkspaceChangeKind.AdditionalDocumentRemoved, true)] + [DataRow(WorkspaceChangeKind.AdditionalDocumentReloaded, true)] + [DataRow(WorkspaceChangeKind.AdditionalDocumentChanged, true)] + [DataRow(WorkspaceChangeKind.AnalyzerConfigDocumentAdded, true)] + [DataRow(WorkspaceChangeKind.AnalyzerConfigDocumentRemoved, true)] + [DataRow(WorkspaceChangeKind.AnalyzerConfigDocumentReloaded, true)] + [DataRow(WorkspaceChangeKind.AnalyzerConfigDocumentChanged, true)] + [DataRow(WorkspaceChangeKind.SolutionAdded, false)] + [DataRow(WorkspaceChangeKind.SolutionRemoved, false)] + [DataRow(WorkspaceChangeKind.SolutionChanged, false)] + [DataRow(WorkspaceChangeKind.ProjectAdded, false)] + [DataRow(WorkspaceChangeKind.ProjectRemoved, false)] + [DataRow(WorkspaceChangeKind.ProjectChanged, false)] + public void IsChangeKindTrivial_ReturnsExpectedResult(WorkspaceChangeKind kind, bool isTrivial) => + new WorkspaceChangeIndicator().IsChangeKindTrivial(kind).Should().Be(isTrivial); +} diff --git a/src/RoslynAnalyzerServer.UnitTests/GlobalUsings.cs b/src/RoslynAnalyzerServer.UnitTests/GlobalUsings.cs new file mode 100644 index 0000000000..8e8fa28ddd --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/GlobalUsings.cs @@ -0,0 +1,25 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +global using System; +global using FluentAssertions; +global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using Moq; +global using NSubstitute; diff --git a/src/RoslynAnalyzerServer.UnitTests/Http/AnalysisRequestHandlerTest.cs b/src/RoslynAnalyzerServer.UnitTests/Http/AnalysisRequestHandlerTest.cs new file mode 100644 index 0000000000..9bc46d989d --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Http/AnalysisRequestHandlerTest.cs @@ -0,0 +1,355 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Net; +using System.Text; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Adapters; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; +using SonarLint.VisualStudio.SLCore.Common.Models; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Http; + +[TestClass] +public class AnalysisRequestHandlerTest +{ + private const string ValidToken = "token"; + private const string InvalidToken = "wrong"; + private const string AnalyzeUrl = "http://localhost/analyze"; + private const string CancelUrl = "http://localhost/cancel"; + private const string UnknownUrl = "http://localhost/SOMERANDOMURL"; + private const int DefaultPort = 1234; + private const int MaxRequestBodyBytes = 100; + private const string AuthTokenHeader = "X-Auth-Token"; + private const string HttpMethodPost = "POST"; + private const string DiagnosticId = "S100"; + private static readonly Guid AnalysisId = Guid.NewGuid(); + private static readonly FileUri FileUri = new("C:\\File.cs"); + private IHttpServerSettings settings = null!; + private IHttpServerConfigurationProvider configurationProvider = null!; + private IHttpListenerContext context = null!; + private ILogger logger = null!; + private IHttpListenerRequest request = null!; + private IHttpListenerResponse response = null!; + private AnalysisRequestHandler testSubject = null!; + + [TestInitialize] + public void TestInitialize() + { + logger = Substitute.For(); + logger.ForContext(Arg.Any()).Returns(logger); + settings = Substitute.For(); + settings.MaxRequestBodyBytes.Returns(MaxRequestBodyBytes); + MockConfigurationProvider(); + testSubject = new AnalysisRequestHandler(logger, settings, configurationProvider); + request = Substitute.For(); + response = Substitute.For(); + context = Substitute.For(); + context.Request.Returns(request); + context.Response.Returns(response); + } + + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport() + ); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public void Ctor_LoggerSetsContext() => logger.Received(1).ForContext(Resources.HttpServerLogContext).ForContext(nameof(RoslynAnalysisHttpServer)); + + [TestMethod] + public void ValidateRequest_RemoteEndpointNull_ReturnsForbidden() + { + request.RemoteEndPoint.Returns((IPEndPoint?)null); + + var result = testSubject.ValidateRequest(context.Request, out var errorCode, out _); + + result.Should().BeFalse(); + errorCode.Should().Be(HttpStatusCode.Forbidden); + } + + [TestMethod] + public void ValidateRequest_NotLocalRequest_ReturnsForbidden() + { + request.RemoteEndPoint.Returns(new IPEndPoint(IPAddress.Parse("8.8.8.8"), DefaultPort)); + + var result = testSubject.ValidateRequest(context.Request, out var errorCode, out _); + + result.Should().BeFalse(); + errorCode.Should().Be(HttpStatusCode.Forbidden); + } + + [TestMethod] + public void ValidateRequest_Loopback_ReturnsOK() + { + MockValidRequest(); + request.RemoteEndPoint.Returns(new IPEndPoint(IPAddress.Loopback, DefaultPort)); + + var result = testSubject.ValidateRequest(context.Request, out _, out _); + + result.Should().BeTrue(); + } + + [TestMethod] + public void ValidateRequest_IPv6Loopback_ReturnsOK() + { + MockValidRequest(); + request.RemoteEndPoint.Returns(new IPEndPoint(IPAddress.IPv6Loopback, DefaultPort)); + + var result = testSubject.ValidateRequest(context.Request, out _, out _); + + result.Should().BeTrue(); + } + + [TestMethod] + public void ValidateRequest_TokenInvalid_ReturnsUnauthorized() + { + MockValidRequest(); + request.Headers.Returns(new WebHeaderCollection { [AuthTokenHeader] = InvalidToken }); + + var result = testSubject.ValidateRequest(context.Request, out var errorCode, out _); + + result.Should().BeFalse(); + errorCode.Should().Be(HttpStatusCode.Unauthorized); + } + + [TestMethod] + [DataRow("Bearer")] + [DataRow("X-Authorization")] + [DataRow("X-Api-Key")] + public void ValidateRequest_TokenInInvalidHeader_ReturnsUnauthorized(string wrongAuthenticationHeader) + { + MockValidRequest(); + request.Headers.Returns(new WebHeaderCollection { [wrongAuthenticationHeader] = InvalidToken }); + + var result = testSubject.ValidateRequest(context.Request, out var errorCode, out _); + + result.Should().BeFalse(); + errorCode.Should().Be(HttpStatusCode.Unauthorized); + } + + [TestMethod] + public void ValidateRequest_ValidInValidHeader_ReturnsOK() + { + MockValidRequest(); + request.Headers.Returns(new WebHeaderCollection { [AuthTokenHeader] = ValidToken }); + + var result = testSubject.ValidateRequest(context.Request, out _, out _); + + result.Should().BeTrue(); + } + + [TestMethod] + [DataRow("http://localhost/invalid")] + [DataRow("http://localhost/analyze/abc")] + public void ValidateRequest_UrlInvalid_ReturnsNotFound(string invalidUrl) + { + MockValidRequest(); + request.Url.Returns(new Uri(invalidUrl)); + + var result = testSubject.ValidateRequest(context.Request, out var errorCode, out _); + + result.Should().BeFalse(); + errorCode.Should().Be(HttpStatusCode.BadRequest); + } + + [TestMethod] + [DataRow("GET")] + [DataRow("PUT")] + [DataRow("PATCH")] + [DataRow("DELETE")] + public void ValidateRequest_MethodInvalid_ReturnsNotFound(string httpMethod) + { + MockValidRequest(); + request.HttpMethod.Returns(httpMethod); + + var result = testSubject.ValidateRequest(context.Request, out var errorCode, out _); + + result.Should().BeFalse(); + errorCode.Should().Be(HttpStatusCode.BadRequest); + } + + [TestMethod] + public void ValidateRequest_ContentLengthExceeded_ReturnsRequestEntityTooLarge() + { + MockValidRequest(); + request.ContentLength64.Returns(MaxRequestBodyBytes + 1); + + var result = testSubject.ValidateRequest(context.Request, out var errorCode, out _); + + result.Should().BeFalse(); + errorCode.Should().Be(HttpStatusCode.RequestEntityTooLarge); + logger.Received().LogVerbose(Resources.BodyLengthExceeded, context.Request.ContentLength64, settings.MaxRequestBodyBytes); + } + + [TestMethod] + public void ValidateRequest_ContentLengthNotExceeded_ReturnsOK() + { + MockValidRequest(); + request.ContentLength64.Returns(MaxRequestBodyBytes); + + var result = testSubject.ValidateRequest(context.Request, out _, out _); + + result.Should().BeTrue(); + } + + [TestMethod] + public void ValidateRequest_AllValid_ReturnsOK() + { + MockValidRequest(); + + var result = testSubject.ValidateRequest(context.Request, out _, out _); + + result.Should().BeTrue(); + } + + [TestMethod] + [DataRow(AnalyzeUrl, RequestType.Analyze)] + [DataRow(CancelUrl, RequestType.Cancel)] + [DataRow(UnknownUrl, RequestType.Unknown)] + public void ValidateRequest_ValidRequestType_SetsCorrectRequestType(string url, RequestType expectedRequestType) + { + MockValidRequest(); + request.Url.Returns(new Uri(url)); + + var result = testSubject.ValidateRequest(context.Request, out _, out var requestType); + + result.Should().Be(expectedRequestType != RequestType.Unknown); + requestType.Should().Be(expectedRequestType); + } + + [TestMethod] + public async Task ParseAnalysisRequestBody_DeserializationFails_ReturnsNull() + { + var unexpectedBodyContent = @" +{ + ""$type"": ""System.Windows.Data.ObjectDataProvider, PresentationFramework"", + ""MethodName"": ""Start"", + ""ObjectInstance"": { + ""$type"": ""System.Diagnostics.Process, System"", + ""StartInfo"": { + ""$type"": ""System.Diagnostics.ProcessStartInfo, System"", + ""FileName"": ""malicious.exe"" + } + } +}"; + var stream = new MemoryStream(Encoding.UTF8.GetBytes(unexpectedBodyContent)); + request.InputStream.Returns(stream); + request.ContentEncoding.Returns(Encoding.UTF8); + + var result = await AnalysisRequestHandler.ParseAnalysisRequestBodyAsync(context.Request); + + result.Should().BeNull(); + } + + [TestMethod] + public async Task ParseAnalysisRequestBody_FileNamesMissing_ReturnsNull() + { + var stream = new MemoryStream("""{"ActiveRules":[]}"""u8.ToArray()); + request.InputStream.Returns(stream); + request.ContentEncoding.Returns(Encoding.UTF8); + + var result = await AnalysisRequestHandler.ParseAnalysisRequestBodyAsync(context.Request); + + result.Should().BeNull(); + } + + [TestMethod] + public async Task ParseAnalysisRequestBody_FileNamesEmpty_ReturnsNull() + { + var stream = new MemoryStream("""{"FileNames":[],"ActiveRules":[]}"""u8.ToArray()); + request.InputStream.Returns(stream); + request.ContentEncoding.Returns(Encoding.UTF8); + + var result = await AnalysisRequestHandler.ParseAnalysisRequestBodyAsync(context.Request); + + result.Should().BeNull(); + } + + [TestMethod] + public async Task ParseAnalysisRequestBody_RequestBodyValid_ReturnsExpectedModel() + { + var validRequestJson = $$"""{"FileUris":["{{FileUri}}"],"ActiveRules":[{"RuleId":"{{DiagnosticId}}"}], "AnalysisId":"{{AnalysisId}}"}"""; + var stream = new MemoryStream(Encoding.UTF8.GetBytes(validRequestJson)); + request.InputStream.Returns(stream); + request.ContentEncoding.Returns(Encoding.UTF8); + + var result = await AnalysisRequestHandler.ParseAnalysisRequestBodyAsync(context.Request); + + result.Should().NotBeNull(); + result!.FileUris.Should().HaveCount(1); + result.FileUris[0].Should().Be(FileUri); + result.ActiveRules.Should().HaveCount(1); + result.ActiveRules[0].RuleId.Should().Be(DiagnosticId); + result.AnalysisId.Should().Be(AnalysisId); + } + + [TestMethod] + public async Task ParseCancellationRequestBody_DeserializationFails_ReturnsNull() + { + var invalidBodyContent = "{}"; + var stream = new MemoryStream(Encoding.UTF8.GetBytes(invalidBodyContent)); + request.InputStream.Returns(stream); + request.ContentEncoding.Returns(Encoding.UTF8); + + var result = await testSubject.ParseCancellationRequestBodyAsync(context.Request); + + result.Should().BeNull(); + } + + [TestMethod] + public async Task ParseCancellationRequestBody_RequestBodyValid_ReturnsExpectedModel() + { + var validRequestJson = $$"""{"AnalysisId":"{{AnalysisId}}"}"""; + var stream = new MemoryStream(Encoding.UTF8.GetBytes(validRequestJson)); + request.InputStream.Returns(stream); + request.ContentEncoding.Returns(Encoding.UTF8); + + var result = await testSubject.ParseCancellationRequestBodyAsync(context.Request); + + result.Should().NotBeNull(); + result!.AnalysisId.Should().Be(AnalysisId); + } + + private void MockValidRequest() + { + request.RemoteEndPoint.Returns(new IPEndPoint(IPAddress.Loopback, DefaultPort)); + request.Headers.Returns(new WebHeaderCollection { [AuthTokenHeader] = ValidToken }); + request.HttpMethod.Returns(HttpMethodPost); + request.Url.Returns(new Uri(AnalyzeUrl)); + request.ContentLength64.Returns(MaxRequestBodyBytes); + } + + private void MockConfigurationProvider() + { + var configuration = Substitute.For(); + configurationProvider = Substitute.For(); + configurationProvider.CurrentConfiguration.Returns(configuration); + configuration.Token.Returns(ValidToken.ToSecureString()); + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Http/HttpListenerCreatorTest.cs b/src/RoslynAnalyzerServer.UnitTests/Http/HttpListenerCreatorTest.cs new file mode 100644 index 0000000000..4ae7cd73ab --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Http/HttpListenerCreatorTest.cs @@ -0,0 +1,45 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Http; + +[TestClass] +public class HttpListenerFactoryTest +{ + private HttpListenerFactory testSubject = null!; + + [TestInitialize] + public void TestInitialize() => testSubject = new HttpListenerFactory(); + + [TestMethod] + [DataRow(8080)] + [DataRow(1234)] + [DataRow(60000)] + public void Create_ShouldReturnListenerWithCorrectPrefix(int port) + { + var listener = testSubject.Create(port); + + listener.Should().NotBeNull(); + listener.Prefixes.Count.Should().Be(1); + listener.Prefixes.Should().Contain($"http://127.0.0.1:{port}/"); + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Http/HttpRequestHandlerTest.cs b/src/RoslynAnalyzerServer.UnitTests/Http/HttpRequestHandlerTest.cs new file mode 100644 index 0000000000..1e5b0bd9d6 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Http/HttpRequestHandlerTest.cs @@ -0,0 +1,113 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.IO; +using System.Net; +using System.Text; +using SonarLint.VisualStudio.Integration.TestInfrastructure; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Adapters; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Http; + +[TestClass] +public class HttpRequestHandlerTest +{ + private IHttpListenerContext context = null!; + private IHttpListenerRequest request = null!; + private IHttpListenerResponse response = null!; + private Stream stream = null!; + private HttpRequestHandler testSubject = null!; + + [TestInitialize] + public void TestInitialize() + { + request = Substitute.For(); + response = Substitute.For(); + context = Substitute.For(); + context.Request.Returns(request); + context.Response.Returns(response); + stream = Substitute.For(); + stream.CanWrite.Returns(true); + context.Response.OutputStream.Returns(stream); + testSubject = new HttpRequestHandler(); + } + + [TestMethod] + [DataRow(HttpStatusCode.BadRequest)] + [DataRow(HttpStatusCode.ServiceUnavailable)] + [DataRow(HttpStatusCode.RequestEntityTooLarge)] + [DataRow(HttpStatusCode.RequestTimeout)] + [DataRow(HttpStatusCode.LengthRequired)] + public void CloseRequest_SetsStatusCodeAndCloses_ReturnsVoid(HttpStatusCode statusCode) + { + testSubject.CloseRequest(context, statusCode); + + response.Received().StatusCode = (int)statusCode; + response.Received().Close(); + } + + [TestMethod] + public void CloseRequest_ConnectionBroken_DoesNotWriteResponse() + { + stream.CanWrite.Returns(false); + + testSubject.CloseRequest(context, HttpStatusCode.BadRequest); + + response.DidNotReceiveWithAnyArgs().StatusCode = default; + response.DidNotReceiveWithAnyArgs().Close(); + } + + [TestMethod] + public async Task SendResponse_WritesCorrectlySerializedDiagnostics() + { + response.OutputStream.Returns(new MemoryStream()); + var expectedString = "{\"Diagnostics\":[{\"Id\":\"id1\"}]}"; + + await testSubject.SendResponseAsync(context, expectedString); + + response.Received().ContentLength64 = Encoding.UTF8.GetBytes(expectedString).Length; + response.Received().StatusCode = (int)HttpStatusCode.OK; + } + + [TestMethod] + public async Task SendResponse_ClosesOutputStream() + { + var outputStream = new MemoryStream(); + response.OutputStream.Returns(outputStream); + + await testSubject.SendResponseAsync(context, "{\"Diagnostics\":[}"); + + outputStream.CanRead.Should().BeFalse(); + } + + [TestMethod] + public async Task SendResponse_ConnectionBroken_DoesNotWriteResponse() + { + stream.CanWrite.Returns(false); + + await testSubject.SendResponseAsync(context, "{\"Diagnostics\":[}"); + + stream.DidNotReceiveWithAnyArgs().WriteAsync(default, default, default, default).IgnoreAwaitForAssert(); + stream.DidNotReceiveWithAnyArgs().Write(default, default, default); + response.DidNotReceiveWithAnyArgs().StatusCode = default; + response.DidNotReceiveWithAnyArgs().Close(); + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Http/HttpServerConfigurationProviderTest.cs b/src/RoslynAnalyzerServer.UnitTests/Http/HttpServerConfigurationProviderTest.cs new file mode 100644 index 0000000000..3e9036defa --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Http/HttpServerConfigurationProviderTest.cs @@ -0,0 +1,124 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Net; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Http; + +[TestClass] +public class HttpServerConfigurationProviderTest +{ + private HttpServerConfigurationProvider testSubject = null!; + + [TestInitialize] + public void TestInitialize() => testSubject = new HttpServerConfigurationProvider(); + + [TestMethod] + public void MefCtor_IHttpServerConfigurationProvider_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported(); + + [TestMethod] + public void MefCtor_IHttpServerConfigurationFactory_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported(); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public void Ctor_CurrentConfigurationNotNull() => testSubject.CurrentConfiguration.Should().NotBeNull(); + + [TestMethod] + public void Ctor_PropertiesAreInitialized() + { + VerifyValidPort(testSubject.CurrentConfiguration.Port); + testSubject.CurrentConfiguration.Token.Should().NotBeNull(); + } + + [TestMethod] + public void Port_MultipleCalls_ReturnsSameValue() + { + var port = testSubject.CurrentConfiguration.Port; + + testSubject.CurrentConfiguration.Port.Should().Be(port); + testSubject.CurrentConfiguration.Port.Should().Be(port); + } + + [TestMethod] + public void Token_MultipleCalls_ReturnsSameValue() + { + var token = testSubject.CurrentConfiguration.Token; + + testSubject.CurrentConfiguration.Token.Should().Be(token); + testSubject.CurrentConfiguration.Token.Should().Be(token); + } + + [TestMethod] + public void Token_HasLength32Bytes() + { + var token = testSubject.CurrentConfiguration.Token; + + Convert.FromBase64String(token.ToUnsecureString()).Length.Should().Be(32); + } + + [TestMethod] + public void SetNewConfiguration_UpdatesCurrentConfiguration() + { + var initialInstance = testSubject.CurrentConfiguration; + + var result = testSubject.SetNewConfiguration(); + + result.Should().NotBeNull(); + result.Should().NotBeSameAs(initialInstance); + testSubject.CurrentConfiguration.Should().BeSameAs(result); + } + + [TestMethod] + public void SetNewConfiguration_GeneratesDifferentProperties() + { + var originalConfiguration = testSubject.CurrentConfiguration; + + var newConfig = testSubject.SetNewConfiguration(); + + newConfig.Port.Should().NotBe(originalConfiguration.Port); + VerifyValidPort(newConfig.Port); + newConfig.Token.Should().NotBe(originalConfiguration.Token); + } + + [TestMethod] + public void MapToInferredProperties_ReturnsExpectedProperties() + { + var portKey = "sonar.sqvsRoslynPlugin.internal.serverPort"; + var tokenKey = "sonar.sqvsRoslynPlugin.internal.serverToken"; + + var analysisProperties = testSubject.CurrentConfiguration.MapToInferredProperties(); + + analysisProperties.Count.Should().Be(2); + analysisProperties.Should().ContainKey(portKey); + analysisProperties.Should().ContainKey(tokenKey); + analysisProperties[portKey].Should().Be(testSubject.CurrentConfiguration.Port.ToString()); + analysisProperties[tokenKey].Should().Be(testSubject.CurrentConfiguration.Token.ToUnsecureString()); + } + + private static void VerifyValidPort(int port) + { + port.Should().BeGreaterThan(IPEndPoint.MinPort); + port.Should().BeLessThan(IPEndPoint.MaxPort); + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Http/HttpServerSettingsTest.cs b/src/RoslynAnalyzerServer.UnitTests/Http/HttpServerSettingsTest.cs new file mode 100644 index 0000000000..2b8164f247 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Http/HttpServerSettingsTest.cs @@ -0,0 +1,51 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Http; + +[TestClass] +public class HttpServerSettingsTest +{ + private HttpServerSettings config = null!; + + [TestInitialize] + public void TestInitialize() => config = new HttpServerSettings(); + + [TestMethod] + public void MefCtor_CheckExports() => MefTestHelpers.CheckTypeCanBeImported(); + + [TestMethod] + public void Mef_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public void MaxStartAttempts_ReturnsTen() => config.MaxStartAttempts.Should().Be(10); + + [TestMethod] + public void RequestMillisecondsTimeout_Returns30SecondsTimeoutInMilliseconds() => config.RequestMillisecondsTimeout.Should().Be(30000); + + [TestMethod] + public void MaxRequestBodyBytes_ReturnsOneMegabyte() => config.MaxRequestBodyBytes.Should().Be(1024 * 1024); + + [TestMethod] + public void MaxConcurrentRequests_ReturnsTwenty() => config.MaxConcurrentRequests.Should().Be(20); +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Http/Models/AnalysisRequestTests.cs b/src/RoslynAnalyzerServer.UnitTests/Http/Models/AnalysisRequestTests.cs new file mode 100644 index 0000000000..d8c51428ef --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Http/Models/AnalysisRequestTests.cs @@ -0,0 +1,66 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Newtonsoft.Json; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; +using SonarLint.VisualStudio.SLCore.Common.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Http.Models; + +[TestClass] +public class AnalysisRequestTests +{ + + [TestMethod] + public void Deserialization_SmokeTest() + { + var json = + """ + { + "FileUris": ["file:///C:/test/file1.cs", "file:///C:/test/file2.cs"], + "ActiveRules": [ + { "RuleId": "S101", "Parameters": { "threshold": "3" } }, + { "RuleId": "S102", "Parameters": { "timeout": "60" } } + ], + "AnalysisProperties": { "prop1": "value1", "prop2": "value2" }, + "AnalyzerInfo": { "ShouldUseCsharpEnterprise": true, "ShouldUseVbEnterprise": false }, + "AnalysisId": "8171cac6-65cc-4ba0-8804-db38f424f37d" + } + """; + + var expected = new AnalysisRequest + { + FileUris = + [ + new FileUri("file:///C:/test/file1.cs"), new FileUri("file:///C:/test/file2.cs") + ], + ActiveRules = + [ + new ActiveRuleDto("S101", new Dictionary { { "threshold", "3" } }), new ActiveRuleDto("S102", new Dictionary { { "timeout", "60" } }) + ], + AnalysisProperties = new Dictionary { { "prop1", "value1" }, { "prop2", "value2" } }, + AnalyzerInfo = new AnalyzerInfoDto(true, false), + AnalysisId = Guid.Parse("8171cac6-65cc-4ba0-8804-db38f424f37d") + }; + + var actual = JsonConvert.DeserializeObject(json); + actual.Should().BeEquivalentTo(expected, options => options.ComparingByMembers().ComparingByMembers().ComparingByMembers()); + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Http/Models/AnalysisResponseTests.cs b/src/RoslynAnalyzerServer.UnitTests/Http/Models/AnalysisResponseTests.cs new file mode 100644 index 0000000000..95d26e50c2 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Http/Models/AnalysisResponseTests.cs @@ -0,0 +1,89 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using FluentAssertions; +using Newtonsoft.Json; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; +using SonarLint.VisualStudio.SLCore.Common.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Http.Models; + +[TestClass] +public class AnalysisResponseTests +{ + [TestMethod] + public void Serialization_ProducesExpectedJson() + { + const string expectedJson = + """ + { + "RoslynIssues": [ + { + "RuleId": "rule-id", + "PrimaryLocation": { + "FileUri": "file:///c:/temp/test.cs", + "Message": "primary message", + "TextRange": { + "StartLine": 1, + "EndLine": 2, + "StartLineOffset": 3, + "EndLineOffset": 4 + } + }, + "Flows": [ + { + "Locations": [ + { + "FileUri": "file:///c:/temp/test.cs", + "Message": "secondary message", + "TextRange": { + "StartLine": 1, + "EndLine": 2, + "StartLineOffset": 3, + "EndLineOffset": 4 + } + } + ] + } + ], + "QuickFixes": [ + { + "Value": "fix value" + } + ] + } + ] + } + """; + var issueTextRange = new RoslynIssueTextRange(1, 2, 3, 4); + var fileUri = new FileUri("file:///c:/temp/test.cs"); + var primaryLocation = new RoslynIssueLocation("primary message", fileUri, issueTextRange); + var secondaryLocation = new RoslynIssueLocation("secondary message", fileUri, issueTextRange); + var flow = new RoslynIssueFlow([secondaryLocation]); + var quickFix = new RoslynIssueQuickFix("fix value"); + var issue = new RoslynIssue("rule-id", primaryLocation, [flow], [quickFix]); + var originalResponse = new AnalysisResponse { RoslynIssues = [issue] }; + + var actualJson = JsonConvert.SerializeObject(originalResponse, Formatting.Indented); + + actualJson.Should().Be(expectedJson); + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Http/RoslynAnalysisHttpServerTest.cs b/src/RoslynAnalyzerServer.UnitTests/Http/RoslynAnalysisHttpServerTest.cs new file mode 100644 index 0000000000..050c277703 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Http/RoslynAnalysisHttpServerTest.cs @@ -0,0 +1,80 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Http; + +[TestClass] +public class RoslynAnalysisHttpServerTest +{ + private static IHttpServerConfigurationFactory _serverConfigurationFactory = null!; + private static IHttpRequestHandler _httpRequestHandler = null!; + private static ILogger _logger = null!; + private static IHttpServerSettings _configuration = null!; + private static IAnalysisRequestHandler _analysisRequestHandler = null!; + private static IRoslynAnalysisService _roslynAnalysisService = null!; + private static RoslynAnalysisHttpServer _testSubject = null!; + + [ClassInitialize] + public static void TestInitialize(TestContext context) + { + _logger = Substitute.For(); + _logger.ForContext(Arg.Any()).Returns(_logger); + _configuration = Substitute.For(); + _analysisRequestHandler = Substitute.For(); + _httpRequestHandler = Substitute.For(); + _roslynAnalysisService = Substitute.For(); + _serverConfigurationFactory = Substitute.For(); + _testSubject = new RoslynAnalysisHttpServer(_logger, _configuration, _analysisRequestHandler, _httpRequestHandler, new HttpListenerFactory(), + _serverConfigurationFactory, _roslynAnalysisService); + } + + [ClassCleanup] + public static void TestCleanup() => _testSubject.Dispose(); + + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public void Ctor_LoggerSetsContext() => _logger.Received(1).ForContext(Resources.RoslynLogContext, Resources.HttpServerLogContext); + + [TestMethod] + public void Dispose_CanBeCalledMultipleTimes() + { + _testSubject.Dispose(); + _testSubject.Dispose(); + + _logger.Received(1).LogVerbose(Resources.HttpServerDisposed); + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs b/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs new file mode 100644 index 0000000000..55b51c4200 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs @@ -0,0 +1,197 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using NSubstitute.ExceptionExtensions; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Integration.TestInfrastructure; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; +using SonarLint.VisualStudio.SLCore.Common.Models; +using SonarLint.VisualStudio.TestInfrastructure; +using Language = SonarLint.VisualStudio.Core.Language; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests; + +[TestClass] +public class RoslynAnalysisServiceTests +{ + private static readonly List DefaultActiveRules = new() { new ActiveRuleDto("sample-rule-id", new Dictionary { { "paramKey", "paramValue" } }) }; + private static readonly Dictionary DefaultAnalysisProperties = new() { { "sonar.cs.any", "any" } }; + private static readonly Dictionary DefaultAnalysisConfigurations = new() { { Language.CSharp, new RoslynAnalysisConfiguration() } }; + private static readonly List DefaultProjectAnalysisRequests = new() { new RoslynProjectAnalysisRequest(Substitute.For(), []) }; + private static readonly List DefaultIssues = new() { new RoslynIssue("sample-rule-id", new RoslynIssueLocation("any", new FileUri("file:///C:/any.cs"), new RoslynIssueTextRange(1, 1, 1, 1))) }; + private static readonly AnalyzerInfoDto DefaultAnalyzerInfoDto = new(false, false); + private IRoslynSolutionAnalysisCommandProvider analysisCommandProvider = null!; + private IRoslynAnalysisConfigurationProvider analysisConfigurationProvider = null!; + + private IRoslynAnalysisEngine analysisEngine = null!; + private IRoslynWorkspaceWrapper workspace = null!; + private RoslynAnalysisService testSubject = null!; + + [TestInitialize] + public void TestInitialize() + { + workspace = Substitute.For(); + analysisEngine = Substitute.For(); + analysisConfigurationProvider = Substitute.For(); + analysisCommandProvider = Substitute.For(); + + testSubject = new RoslynAnalysisService(workspace, analysisEngine, analysisConfigurationProvider, analysisCommandProvider); + } + + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public async Task AnalyzeAsync_PassesCorrectArgumentsToEngine() + { + string[] filePaths = [@"C:\file1.cs", @"C:\folder\file2.cs"]; + SetUpBasicAnalysisServices(filePaths); + analysisEngine.AnalyzeAsync(DefaultProjectAnalysisRequests, DefaultAnalysisConfigurations, Arg.Any()).Returns(DefaultIssues); + + var analysisRequest = CreateAnalysisRequest(filePaths.Select(x => new FileUri(x)).ToList()); + + var issues = await testSubject.AnalyzeAsync(analysisRequest, CancellationToken.None); + + issues.Should().BeSameAs(DefaultIssues); + } + + [TestMethod] + public void Cancel_NonExistingId_ReturnsFalse() + { + var nonExistingId = Guid.NewGuid(); + var cancellationRequest = CreateCancellationRequest(nonExistingId); + + var result = testSubject.Cancel(cancellationRequest); + + result.Should().BeFalse(); + } + + [TestMethod] + public void Cancel_ExistingId_ReturnsTrueAndCancelsToken() + { + var analysisId = Guid.NewGuid(); + var analysisRequest = CreateAnalysisRequest(analysisId: analysisId); + + SetUpBasicAnalysisServices(); + + var taskCompletionSource = new TaskCompletionSource>(); + var internalAnalysisToken = CancellationToken.None; + + analysisEngine.AnalyzeAsync( + Arg.Any>(), + Arg.Any>(), + Arg.Do(t => internalAnalysisToken = t)) + .Returns(taskCompletionSource.Task); + + testSubject.AnalyzeAsync(analysisRequest, CancellationToken.None).IgnoreAwaitForAssert(); + var cancellationRequest = CreateCancellationRequest(analysisId); + + var result = testSubject.Cancel(cancellationRequest); + + result.Should().BeTrue(); + taskCompletionSource.SetResult(DefaultIssues); + internalAnalysisToken.IsCancellationRequested.Should().BeTrue(); + } + + [TestMethod] + public async Task AnalyzeAsync_TokenRemovedAfterAnalysis_EvenIfAnalysisSucceeds() + { + var analysisId = Guid.NewGuid(); + var analysisRequest = CreateAnalysisRequest(analysisId: analysisId); + SetUpBasicAnalysisServices(); + analysisEngine + .AnalyzeAsync(Arg.Any>(), Arg.Any>(), Arg.Any()) + .Returns(DefaultIssues); + + await testSubject.AnalyzeAsync(analysisRequest, CancellationToken.None); + + var cancellationRequest = CreateCancellationRequest(analysisId); + var result = testSubject.Cancel(cancellationRequest); + result.Should().BeFalse(); + } + + [TestMethod] + public void AnalyzeAsync_TokenRemovedAfterAnalysis_EvenIfAnalysisThrows() + { + var analysisId = Guid.NewGuid(); + var analysisRequest = CreateAnalysisRequest(analysisId: analysisId); + SetUpBasicAnalysisServices(); + analysisEngine + .AnalyzeAsync(Arg.Any>(), Arg.Any>(), Arg.Any()) + .ThrowsAsync(new InvalidOperationException("Test exception")); + + var act = () => testSubject.AnalyzeAsync(analysisRequest, CancellationToken.None); + act.Should().ThrowAsync().IgnoreAwaitForAssert(); + + var cancellationRequest = CreateCancellationRequest(analysisId); + var result = testSubject.Cancel(cancellationRequest); + result.Should().BeFalse(); + } + + [TestMethod] + public void Dispose_DisposesWorkspace() + { + testSubject.Dispose(); + + workspace.Received().Dispose(); + } + + private void SetUpConfigurationProvider() => + analysisConfigurationProvider + .GetConfigurationAsync(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto) + .Returns(DefaultAnalysisConfigurations); + + private void SetUpBasicAnalysisServices(string[]? filePaths = null) + { + SetUpConfigurationProvider(); + analysisCommandProvider + .GetAnalysisCommandsForCurrentSolution(filePaths is not null ? Arg.Is(x => x.SequenceEqual(filePaths)) : Arg.Any()) + .Returns(DefaultProjectAnalysisRequests); + } + + private static AnalysisRequest CreateAnalysisRequest( + List? fileNames = null, + Guid? analysisId = null) + { + fileNames ??= [new FileUri(@"C:\file1.cs")]; + + return new AnalysisRequest + { + FileUris = fileNames, + ActiveRules = DefaultActiveRules, + AnalysisProperties = DefaultAnalysisProperties, + AnalyzerInfo = DefaultAnalyzerInfoDto, + AnalysisId = analysisId ?? Guid.NewGuid() + }; + } + + private static AnalysisCancellationRequest CreateCancellationRequest(Guid analysisId) => new() { AnalysisId = analysisId }; +} diff --git a/src/RoslynAnalyzerServer.UnitTests/RoslynAnalyzerServer.UnitTests.csproj b/src/RoslynAnalyzerServer.UnitTests/RoslynAnalyzerServer.UnitTests.csproj new file mode 100644 index 0000000000..d18684008d --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/RoslynAnalyzerServer.UnitTests.csproj @@ -0,0 +1,24 @@ + + + + + + SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests + SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests + enable + enable + true + + + + + + + + + + + + + + diff --git a/src/RoslynAnalyzerServer.UnitTests/RoslynQuickFixApplicationImplTests.cs b/src/RoslynAnalyzerServer.UnitTests/RoslynQuickFixApplicationImplTests.cs new file mode 100644 index 0000000000..3b5319a8a4 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/RoslynQuickFixApplicationImplTests.cs @@ -0,0 +1,123 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using SonarLint.VisualStudio.Integration.TestInfrastructure; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests; + +[TestClass] +public class RoslynQuickFixApplicationImplTests +{ + private IRoslynWorkspaceWrapper workspace = null!; + private IRoslynSolutionWrapper originalSolution = null!; + private IRoslynCodeActionWrapper codeAction = null!; + private CancellationToken cancellationToken; + private RoslynQuickFixApplicationImpl testSubject = null!; + + [TestInitialize] + public void TestInitialize() + { + cancellationToken = new CancellationToken(); + workspace = Substitute.For(); + originalSolution = Substitute.For(); + codeAction = Substitute.For(); + + testSubject = new RoslynQuickFixApplicationImpl(workspace, originalSolution, codeAction); + } + + [TestMethod] + public void Message_ReturnsCodeActionTitle() + { + const string title = "Test title"; + codeAction.Title.Returns(title); + + testSubject.Message.Should().Be(title); + } + + [TestMethod] + public async Task ApplyAsync_NoApplyChangesOperation_DoesNotApplyChanges() + { + codeAction.GetOperationsAsync(cancellationToken).Returns(ImmutableArray.Create(Substitute.For())); + + var result = await testSubject.ApplyAsync(cancellationToken); + + VerifyNotAppliedChanges(result); + } + + [TestMethod] + public async Task ApplyAsync_MultipleOperationsWithApplyChangesOperation_DoesNotApplyChanges() + { + var operations = ImmutableArray.Create(new Microsoft.CodeAnalysis.CodeActions.ApplyChangesOperation(CreateDummySolution()), Substitute.For()); + codeAction.GetOperationsAsync(cancellationToken).Returns(operations); + + var result = await testSubject.ApplyAsync(cancellationToken); + + VerifyNotAppliedChanges(result); + } + + [TestMethod] + public async Task ApplyAsync_HasApplyChangesOperation_CallsWorkspaceApplyChanges() + { + var applyChangesOperation = new Microsoft.CodeAnalysis.CodeActions.ApplyChangesOperation(CreateDummySolution()); + var operations = ImmutableArray.Create(applyChangesOperation); + codeAction.GetOperationsAsync(cancellationToken).Returns(operations); + workspace.ApplyOrMergeChangesAsync(originalSolution, applyChangesOperation, cancellationToken).Returns(true); + + var result = await testSubject.ApplyAsync(cancellationToken); + + result.Should().BeTrue(); + await workspace.Received(1).ApplyOrMergeChangesAsync(originalSolution, applyChangesOperation, cancellationToken); + } + + [TestMethod] + public async Task ApplyAsync_WorkspaceApplyChangesFails_ReturnsFalse() + { + var applyChangesOperation = new Microsoft.CodeAnalysis.CodeActions.ApplyChangesOperation(CreateDummySolution()); + var operations = ImmutableArray.Create(applyChangesOperation); + codeAction.GetOperationsAsync(cancellationToken).Returns(operations); + workspace.ApplyOrMergeChangesAsync(originalSolution, applyChangesOperation, cancellationToken).Returns(false); + + var result = await testSubject.ApplyAsync(cancellationToken); + + result.Should().BeFalse(); + await workspace.Received(1).ApplyOrMergeChangesAsync(originalSolution, applyChangesOperation, cancellationToken); + } + + private void VerifyNotAppliedChanges(bool result) + { + result.Should().BeFalse(); + workspace.DidNotReceiveWithAnyArgs().ApplyOrMergeChangesAsync(default!, default!, default).IgnoreAwaitForAssert(); + } + + private Solution CreateDummySolution() + { + var adhocWorkspace = new AdhocWorkspace(); + + var slnInfo = SolutionInfo.Create(SolutionId.CreateNewId(), VersionStamp.Default, null, + []); + adhocWorkspace.AddSolution(slnInfo); + + return adhocWorkspace.CurrentSolution; + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/app.config b/src/RoslynAnalyzerServer.UnitTests/app.config new file mode 100644 index 0000000000..7f9314106a --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/app.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/packages.lock.json b/src/RoslynAnalyzerServer.UnitTests/packages.lock.json similarity index 95% rename from src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/packages.lock.json rename to src/RoslynAnalyzerServer.UnitTests/packages.lock.json index 53977d1d7c..ef108b8da2 100644 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/packages.lock.json +++ b/src/RoslynAnalyzerServer.UnitTests/packages.lock.json @@ -38,6 +38,19 @@ "Microsoft.CodeAnalysis.Common": "[3.11.0]" } }, + "Microsoft.CodeAnalysis.Workspaces.Common": { + "type": "Direct", + "requested": "[3.11.0, )", + "resolved": "3.11.0", + "contentHash": "YAbH4LCJfh8DhDGwYzSHqvnF06lKkVwblr8C+GwIYCv0i3Rzqjnbversat+i2n9k8twQ43yxVGTYK5p/mIOj4w==", + "dependencies": { + "Humanizer.Core": "2.2.0", + "Microsoft.Bcl.AsyncInterfaces": "5.0.0", + "Microsoft.CodeAnalysis.Common": "[3.11.0]", + "System.Composition": "1.0.31", + "System.IO.Pipelines": "5.0.1" + } + }, "Microsoft.NET.Test.Sdk": { "type": "Direct", "requested": "[16.6.1, )", @@ -135,15 +148,10 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { + "Humanizer.Core": { "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" + "resolved": "2.2.0", + "contentHash": "rsYXB7+iUPP8AHgQ8JP2UZI2xK2KhjcdGr9E6zX3CsZaTLCaw8M35vaAJRo1rfxeaZEVMuXeaquLVCkZ7JcZ5Q==" }, "MessagePack": { "type": "Transitive", @@ -994,6 +1002,54 @@ "resolved": "4.5.0", "contentHash": "+iB9FoZnfdqMEGq6np28X6YNSUrse16CakmIhV3h6PxEWt7jYxUN3Txs1D8MZhhf4QmyvK0F/EcIN0f4gGN0dA==" }, + "System.Composition": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "I+D26qpYdoklyAVUdqwUBrEIckMNjAYnuPJy/h9dsQItpQwVREkDFs4b4tkBza0kT2Yk48Lcfsv2QQ9hWsh9Iw==", + "dependencies": { + "System.Composition.AttributedModel": "1.0.31", + "System.Composition.Convention": "1.0.31", + "System.Composition.Hosting": "1.0.31", + "System.Composition.Runtime": "1.0.31", + "System.Composition.TypedParts": "1.0.31" + } + }, + "System.Composition.AttributedModel": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "NHWhkM3ZkspmA0XJEsKdtTt1ViDYuojgSND3yHhTzwxepiwqZf+BCWuvCbjUt4fe0NxxQhUDGJ5km6sLjo9qnQ==" + }, + "System.Composition.Convention": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "GLjh2Ju71k6C0qxMMtl4efHa68NmWeIUYh4fkUI8xbjQrEBvFmRwMDFcylT8/PR9SQbeeL48IkFxU/+gd0nYEQ==", + "dependencies": { + "System.Composition.AttributedModel": "1.0.31" + } + }, + "System.Composition.Hosting": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "fN1bT4RX4vUqjbgoyuJFVUizAl2mYF5VAb+bVIxIYZSSc0BdnX+yGAxcavxJuDDCQ1K+/mdpgyEFc8e9ikjvrg==", + "dependencies": { + "System.Composition.Runtime": "1.0.31" + } + }, + "System.Composition.Runtime": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "0LEJN+2NVM89CE4SekDrrk5tHV5LeATltkp+9WNYrR+Huiyt0vaCqHbbHtVAjPyeLWIc8dOz/3kthRBj32wGQg==" + }, + "System.Composition.TypedParts": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "0Zae/FtzeFgDBBuILeIbC/T9HMYbW4olAmi8XqqAGosSOWvXfiQLfARZEhiGd0LVXaYgXr0NhxiU1LldRP1fpQ==", + "dependencies": { + "System.Composition.AttributedModel": "1.0.31", + "System.Composition.Hosting": "1.0.31", + "System.Composition.Runtime": "1.0.31" + } + }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", "resolved": "5.0.1", @@ -1343,16 +1399,10 @@ "SonarQube.Client": "[1.0.0, )" } }, - "SonarLint.VisualStudio.Roslyn.Suppressions": { + "SonarLint.VisualStudio.RoslynAnalyzerServer": { "type": "Project", "dependencies": { - "Microsoft.VisualStudio.Sdk": "[17.0.31902.203, )", - "Newtonsoft.Json": "[13.0.3, )", - "SonarLint.VisualStudio.ConnectedMode": "[1.0.0, )", - "SonarLint.VisualStudio.Core": "[1.0.0, )", - "SonarLint.VisualStudio.Infrastructure.VS": "[1.0.0, )", - "SonarQube.Client": "[1.0.0, )", - "System.IO.Abstractions": "[9.0.4, )" + "SonarLint.VisualStudio.Core": "[1.0.0, )" } }, "SonarLint.VisualStudio.SLCore": { @@ -1365,8 +1415,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/AnalysisConfigurationParametersCache.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/AnalysisConfigurationParametersCache.cs new file mode 100644 index 0000000000..0f3559be0b --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/AnalysisConfigurationParametersCache.cs @@ -0,0 +1,76 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +internal sealed record AnalysisConfigurationParametersCache( + Dictionary ActiveRuleDtos, + Dictionary AnalysisProperties, + AnalyzerInfoDto AnalyzerInfo); + +internal static class AnalysisConfigurationParametersCacheExtensions +{ + public static bool ShouldInvalidateCache( + this AnalysisConfigurationParametersCache? cache, + List newActiveRuleDtos, + Dictionary newAnalysisProperties, + AnalyzerInfoDto analyzerInfo) => + cache == null + || cache.AnalyzerInfo != analyzerInfo + || !AreSameActiveRuleDtos(newActiveRuleDtos, cache.ActiveRuleDtos) + || !AreDictionariesEqual(newAnalysisProperties, cache.AnalysisProperties); + + private static bool AreSameActiveRuleDtos(List newActiveRuleDtos, Dictionary oldActiveRuleDtos) + { + if (oldActiveRuleDtos.Count != newActiveRuleDtos.Count) + { + return false; + } + + foreach (var newRule in newActiveRuleDtos) + { + if (!oldActiveRuleDtos.TryGetValue(newRule.RuleId, out var cachedActiveRuleDto) || + !AreDictionariesEqual(newRule.Parameters, cachedActiveRuleDto.Parameters)) + { + return false; + } + } + return true; + } + + private static bool AreDictionariesEqual(Dictionary newDictionary, Dictionary oldDictionary) + { + if (newDictionary.Count != oldDictionary.Count) + { + return false; + } + + foreach (var newKvp in newDictionary) + { + if (!oldDictionary.TryGetValue(newKvp.Key, out var oldValue) || oldValue != newKvp.Value) + { + return false; + } + } + return true; + } +} diff --git a/src/Infrastructure.VS/Roslyn/IEmbeddedRoslynAnalyzersLocator.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/IEmbeddedRoslynAnalyzersLocator.cs similarity index 75% rename from src/Infrastructure.VS/Roslyn/IEmbeddedRoslynAnalyzersLocator.cs rename to src/RoslynAnalyzerServer/Analysis/Configuration/IEmbeddedRoslynAnalyzersLocator.cs index de06ed41c2..33f5221bf1 100644 --- a/src/Infrastructure.VS/Roslyn/IEmbeddedRoslynAnalyzersLocator.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/IEmbeddedRoslynAnalyzersLocator.cs @@ -18,11 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -namespace SonarLint.VisualStudio.Infrastructure.VS.Roslyn; +using SonarLint.VisualStudio.Core; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; public interface IEmbeddedDotnetAnalyzersLocator { - List GetBasicAnalyzerFullPaths(); - - List GetEnterpriseAnalyzerFullPaths(); + Dictionary> GetAnalyzerFullPathsByLicensedLanguage(); } + +public record LicensedRoslynLanguage(RoslynLanguage RoslynLanguage, bool IsEnterprise); diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisConfigurationProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisConfigurationProvider.cs new file mode 100644 index 0000000000..9576934609 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisConfigurationProvider.cs @@ -0,0 +1,32 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +internal interface IRoslynAnalysisConfigurationProvider +{ + Task> GetConfigurationAsync( + List activeRules, + Dictionary analysisProperties, + AnalyzerInfoDto analyzerInfo); +} diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisProfilesProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisProfilesProvider.cs new file mode 100644 index 0000000000..c13035f0cf --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisProfilesProvider.cs @@ -0,0 +1,41 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +internal interface IRoslynAnalysisProfilesProvider +{ + Dictionary GetAnalysisProfilesByLanguage( + ImmutableDictionary analyzerAssemblyContents, + List activeRules, + Dictionary? analysisProperties); +} + +internal record struct RoslynAnalysisProfile( + ImmutableArray Analyzers, + ImmutableDictionary> CodeFixProvidersByRuleKey, + List Rules, + Dictionary AnalysisProperties); diff --git a/src/Infrastructure.VS/Roslyn/IBasicRoslynAnalyzerProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerLoader.cs similarity index 69% rename from src/Infrastructure.VS/Roslyn/IBasicRoslynAnalyzerProvider.cs rename to src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerLoader.cs index 515ed39f52..84e7663d74 100644 --- a/src/Infrastructure.VS/Roslyn/IBasicRoslynAnalyzerProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerLoader.cs @@ -18,16 +18,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Collections.Immutable; +using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Diagnostics; -namespace SonarLint.VisualStudio.Infrastructure.VS.Roslyn; +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; -public interface IBasicRoslynAnalyzerProvider +internal interface IRoslynAnalyzerLoader { - /// - /// Returns SonarAnalyzer.CSharp & SonarAnalyzer.VisualBasic analyzer DLLs that are embedded in the VSIX. - /// If no analyzer is found, throws an exception - /// - Task> GetBasicAsync(); + LoadedAnalyzerClasses LoadAnalyzerAssembly(string filePath); } + +internal readonly record struct LoadedAnalyzerClasses(IReadOnlyCollection Analyzers, IReadOnlyCollection CodeFixProviders); diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerProvider.cs new file mode 100644 index 0000000000..6c9d47dc9d --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerProvider.cs @@ -0,0 +1,42 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +internal interface IRoslynAnalyzerProvider +{ + ImmutableDictionary LoadAndProcessAnalyzerAssemblies(AnalyzerInfoDto analyzerInfo); +} + +public interface IRoslynAnalyzerAssemblyContentsLoader +{ + void LoadRoslynAnalyzerAssemblyContentsIfNeeded(); +} + +internal readonly record struct AnalyzerAssemblyContents( + ImmutableArray Analyzers, + ImmutableHashSet SupportedRuleKeys, + ImmutableDictionary> CodeFixProvidersByRuleKey); diff --git a/src/SonarQube.Client/Api/IGetSonarLintEventStream.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/ISonarLintConfigurationXmlSerializer.cs similarity index 78% rename from src/SonarQube.Client/Api/IGetSonarLintEventStream.cs rename to src/RoslynAnalyzerServer/Analysis/Configuration/ISonarLintConfigurationXmlSerializer.cs index 9be957d125..f8a830b5e2 100644 --- a/src/SonarQube.Client/Api/IGetSonarLintEventStream.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/ISonarLintConfigurationXmlSerializer.cs @@ -18,14 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.IO; -using SonarQube.Client.Requests; +using SonarLint.VisualStudio.Core.CSharpVB; -namespace SonarQube.Client.Api +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +internal interface ISonarLintConfigurationXmlSerializer { - internal interface IGetSonarLintEventStream : IRequest - { - string Languages { get; set; } - string ProjectKey { get; set; } - } + string Serialize(SonarLintConfiguration configuration); } diff --git a/src/SonarQube.Client/Api/IGetPropertiesRequest.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/ISonarLintXmlProvider.cs similarity index 79% rename from src/SonarQube.Client/Api/IGetPropertiesRequest.cs rename to src/RoslynAnalyzerServer/Analysis/Configuration/ISonarLintXmlProvider.cs index 42932eff18..ffa8498c24 100644 --- a/src/SonarQube.Client/Api/IGetPropertiesRequest.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/ISonarLintXmlProvider.cs @@ -18,13 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using SonarQube.Client.Models; -using SonarQube.Client.Requests; +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; -namespace SonarQube.Client.Api +internal interface ISonarLintXmlProvider { - public interface IGetPropertiesRequest : IRequest - { - string ProjectKey { get; set; } - } + SonarLintXmlConfigurationFile Create(RoslynAnalysisProfile analysisProfile); } diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisConfigurationProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisConfigurationProvider.cs new file mode 100644 index 0000000000..71012165e8 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisConfigurationProvider.cs @@ -0,0 +1,107 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using System.ComponentModel.Composition; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Synchronization; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +[Export(typeof(IRoslynAnalysisConfigurationProvider))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +internal class RoslynAnalysisConfigurationProvider( + ISonarLintXmlProvider sonarLintXmlProvider, + IRoslynAnalyzerProvider roslynAnalyzerProvider, + IRoslynAnalysisProfilesProvider analyzerProfilesProvider, + IAsyncLockFactory asyncLockFactory, + IThreadHandling threadHandling, + ILogger logger) : IRoslynAnalysisConfigurationProvider +{ + private readonly IAsyncLock asyncLock = asyncLockFactory.Create(); + private readonly ILogger logger = logger.ForContext(Resources.RoslynLogContext, Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisConfigurationLogContext); + private AnalysisConfigurationCache? cache; + + public Task> GetConfigurationAsync( + List activeRules, + Dictionary analysisProperties, + AnalyzerInfoDto analyzerInfo) => + threadHandling.RunOnBackgroundThread(async () => + { + using (await asyncLock.AcquireAsync()) + { + if (!cache.HasValue || cache.Value.Parameters.ShouldInvalidateCache(activeRules, analysisProperties, analyzerInfo)) + { + BuildConfigurations(activeRules, analysisProperties, analyzerInfo); + } + return cache!.Value.Configurations; + } + }); + + private void BuildConfigurations( + List activeRules, + Dictionary analysisProperties, + AnalyzerInfoDto analyzerInfo) + { + var analyzerAssemblyContents = roslynAnalyzerProvider.LoadAndProcessAnalyzerAssemblies(analyzerInfo); + var analysisProfilesByLanguage = analyzerProfilesProvider.GetAnalysisProfilesByLanguage(analyzerAssemblyContents, activeRules, analysisProperties); + var activeRulesMap = activeRules.ToDictionary(r => r.RuleId, r => r); + cache = new AnalysisConfigurationCache( + new AnalysisConfigurationParametersCache(activeRulesMap, analysisProperties, analyzerInfo), + BuildConfigurations(analysisProfilesByLanguage)); + } + + private IReadOnlyDictionary BuildConfigurations(Dictionary analysisProfilesByLanguage) + { + var configurations = new Dictionary(); + foreach (var analyzerAndLanguage in analysisProfilesByLanguage) + { + var language = analyzerAndLanguage.Key; + var analysisProfile = analyzerAndLanguage.Value; + + var languageLogContext = new MessageLevelContext { VerboseContext = [language.Id] }; + + if (analysisProfile is not { Analyzers.Length: > 0 }) + { + logger.LogVerbose(languageLogContext, Resources.RoslynAnalysisConfigurationNoAnalyzers, language.Name); + continue; + } + + if (!analysisProfile.Rules.Any(r => r.IsActive)) + { + logger.LogVerbose(languageLogContext, Resources.RoslynAnalysisConfigurationNoActiveRules, language.Name); + continue; + } + + configurations.Add( + language, + new RoslynAnalysisConfiguration( + sonarLintXmlProvider.Create(analysisProfile), + analysisProfile.Rules.ToImmutableDictionary(x => x.RuleId.RuleKey, y => y.ReportDiagnostic), + analysisProfile.Analyzers, + analysisProfile.CodeFixProvidersByRuleKey)); + } + return configurations; + } + + private record struct AnalysisConfigurationCache(AnalysisConfigurationParametersCache Parameters, IReadOnlyDictionary Configurations); +} diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisProfilesProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisProfilesProvider.cs new file mode 100644 index 0000000000..ac809ab53f --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisProfilesProvider.cs @@ -0,0 +1,94 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using System.ComponentModel.Composition; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +[Export(typeof(IRoslynAnalysisProfilesProvider))] +[PartCreationPolicy(CreationPolicy.Shared)] +internal class RoslynAnalysisProfilesProvider : IRoslynAnalysisProfilesProvider +{ + public Dictionary GetAnalysisProfilesByLanguage( + ImmutableDictionary analyzerAssemblyContents, + List activeRules, + Dictionary? analysisProperties) + { + var roslynAnalysisProfiles = InitializeProfilesForEachLanguage(analyzerAssemblyContents); + AddRules(activeRules, analyzerAssemblyContents, roslynAnalysisProfiles); + AddProperties(analysisProperties, roslynAnalysisProfiles); + + return roslynAnalysisProfiles; + } + + private static Dictionary InitializeProfilesForEachLanguage(ImmutableDictionary analyzerAssemblyContents) => + analyzerAssemblyContents.ToDictionary( + x => x.Key, + x => new RoslynAnalysisProfile(x.Value.Analyzers, x.Value.CodeFixProvidersByRuleKey, [], [])); + + private static void AddRules( + List activeRules, + ImmutableDictionary supportedRulesByLanguage, + Dictionary roslynAnalysisProfiles) + { + var activeRulesById = activeRules.ToDictionary(x => x.RuleId, y => y); + + foreach (var kvp in supportedRulesByLanguage) + { + var language = kvp.Key; + + if (!roslynAnalysisProfiles.TryGetValue(language, out var analysisProfile)) + { + continue; + } + + foreach (var ruleId in kvp.Value.SupportedRuleKeys.Select(ruleKey => new SonarCompositeRuleId(language.RepoInfo.Key, ruleKey))) + { + analysisProfile.Rules.Add(activeRulesById.TryGetValue(ruleId.ErrorListErrorCode, out var activeRule) + ? new RoslynRuleConfiguration(ruleId, true, activeRule.Parameters) + : new RoslynRuleConfiguration(ruleId, false, null)); + } + } + } + + private static void AddProperties(Dictionary? analysisProperties, Dictionary roslynAnalysisProfiles) + { + if (analysisProperties == null) + { + return; + } + + foreach (var languageAndProfile in roslynAnalysisProfiles) + { + var prefix = $"sonar.{languageAndProfile.Key.ServerLanguageKey}."; + + foreach (var analysisProperty in analysisProperties.Where(analysisProperty => IsPropertyForLanguage(prefix, analysisProperty.Key))) + { + languageAndProfile.Value.AnalysisProperties.Add(analysisProperty.Key, analysisProperty.Value); + } + } + } + + private static bool IsPropertyForLanguage(string prefix, string propertyKey) => + propertyKey.StartsWith(prefix) && propertyKey.Length > prefix.Length; +} diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerLoader.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerLoader.cs new file mode 100644 index 0000000000..ae41b75d01 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerLoader.cs @@ -0,0 +1,83 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using SonarLint.VisualStudio.Core; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +[Export(typeof(IRoslynAnalyzerLoader))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +[ExcludeFromCodeCoverage] +internal class RoslynAnalyzerLoader(ILogger logger) : IRoslynAnalyzerLoader +{ + private readonly ILogger logger = logger.ForContext(Resources.RoslynLogContext, Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisAnalyzerLoaderLogContext); + + public LoadedAnalyzerClasses LoadAnalyzerAssembly(string filePath) + { + try + { + var analyzers = new List(); + var codeFixProviders = new List(); + + foreach (var type in Assembly.LoadFrom(filePath).GetTypes().Where(t => t is { IsAbstract: false, IsInterface: false, IsGenericType: false })) + { + if (TryLoadType(type, out CodeFixProvider? codeFixProvider, filePath)) + { + codeFixProviders.Add(codeFixProvider); + } + else if (TryLoadType(type, out DiagnosticAnalyzer? analyzer, filePath)) + { + analyzers.Add(analyzer); + } + } + + return new LoadedAnalyzerClasses(analyzers, codeFixProviders); + } + catch (Exception e) + { + logger.WriteLine(Resources.RoslynAnalysisAnalyzerLoaderFailedToLoad, filePath, e); + return new LoadedAnalyzerClasses([], []); + } + } + + private bool TryLoadType(Type type, [NotNullWhen(true)] out T? value, string originAssembly) where T : class + { + value = null; + try + { + if (typeof(T).IsAssignableFrom(type)) + { + value = (T)Activator.CreateInstance(type); + return true; + } + } + catch (Exception e) + { + logger.LogVerbose(Resources.RoslynAnalysisAnalyzerClassLoaderFailedToLoad, type, originAssembly, e); + } + return false; + } +} diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerProvider.cs new file mode 100644 index 0000000000..068a93301c --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerProvider.cs @@ -0,0 +1,110 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using System.ComponentModel.Composition; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +[Export(typeof(IRoslynAnalyzerProvider))] +[Export(typeof(IRoslynAnalyzerAssemblyContentsLoader))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +internal class RoslynAnalyzerProvider(IEmbeddedDotnetAnalyzersLocator analyzersLocator, IRoslynAnalyzerLoader roslynAnalyzerLoader) : IRoslynAnalyzerProvider, IRoslynAnalyzerAssemblyContentsLoader +{ + private ImmutableDictionary? cachedAnalyzerAssemblyContents; + private static readonly object LockObj = new(); + + public ImmutableDictionary LoadAndProcessAnalyzerAssemblies(AnalyzerInfoDto analyzerInfo) + { + LoadRoslynAnalyzerAssemblyContentsIfNeeded(); + + return cachedAnalyzerAssemblyContents! + .Where(kvp => FilterByLicense(kvp, analyzerInfo)) + .ToDictionary(kvp => kvp.Key.RoslynLanguage, kvp => kvp.Value) + .ToImmutableDictionary(); + } + + public void LoadRoslynAnalyzerAssemblyContentsIfNeeded() + { + lock (LockObj) + { + if (cachedAnalyzerAssemblyContents != null) + { + return; + } + var analyzerFullPathsByLanguage = analyzersLocator.GetAnalyzerFullPathsByLicensedLanguage(); + cachedAnalyzerAssemblyContents = LoadFromAssemblies(analyzerFullPathsByLanguage); + } + } + + private static bool FilterByLicense(KeyValuePair kvp, AnalyzerInfoDto analyzerInfo) + { + if (kvp.Key.RoslynLanguage.Equals(Language.VBNET)) + { + return kvp.Key.IsEnterprise == analyzerInfo.ShouldUseVbEnterprise; + } + + return kvp.Key.IsEnterprise == analyzerInfo.ShouldUseCsharpEnterprise; + } + + private ImmutableDictionary LoadFromAssemblies(Dictionary> analyzerFullPathsByLanguage) + { + var builder = ImmutableDictionary.CreateBuilder(); + + foreach (var languageAndAnalyzers in analyzerFullPathsByLanguage) + { + var supportedDiagnostics = ImmutableHashSet.CreateBuilder(); + var analyzers = ImmutableArray.CreateBuilder(); + var codeFixProviders = ImmutableDictionary.CreateBuilder>(); + + foreach (var assemblyContents in languageAndAnalyzers.Value.Select(roslynAnalyzerLoader.LoadAnalyzerAssembly)) + { + analyzers.AddRange(assemblyContents.Analyzers); + supportedDiagnostics.UnionWith(assemblyContents.Analyzers.SelectMany(x => x.SupportedDiagnostics.Select(y => y.Id))); + AddCodeFixProviders(assemblyContents, codeFixProviders); + } + + var immutableArray = supportedDiagnostics.ToImmutable(); + builder.Add(languageAndAnalyzers.Key, new AnalyzerAssemblyContents(analyzers.ToImmutable(), immutableArray.ToImmutableHashSet(), codeFixProviders.ToImmutable())); + } + + return builder.ToImmutable(); + } + + private static void AddCodeFixProviders(LoadedAnalyzerClasses classes, ImmutableDictionary>.Builder codeFixProviders) + { + foreach (var codeFixProvider in classes.CodeFixProviders) + { + foreach (var fixableDiagnosticId in codeFixProvider.FixableDiagnosticIds) + { + if (!codeFixProviders.ContainsKey(fixableDiagnosticId)) + { + codeFixProviders[fixableDiagnosticId] = new List(); + } + ((List)codeFixProviders[fixableDiagnosticId]).Add(codeFixProvider); + } + } + } +} diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynRuleConfiguration.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynRuleConfiguration.cs new file mode 100644 index 0000000000..4fc62bbdc9 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynRuleConfiguration.cs @@ -0,0 +1,29 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.CodeAnalysis; +using SonarLint.VisualStudio.Core; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +internal readonly record struct RoslynRuleConfiguration(SonarCompositeRuleId RuleId, bool IsActive, Dictionary? Parameters) +{ + public ReportDiagnostic ReportDiagnostic => IsActive ? ReportDiagnostic.Warn : ReportDiagnostic.Suppress; +} diff --git a/src/Integration/CSharpVB/SonarLintConfigXmlSerializer.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/SonarLintConfigurationXmlSerializer.cs similarity index 92% rename from src/Integration/CSharpVB/SonarLintConfigXmlSerializer.cs rename to src/RoslynAnalyzerServer/Analysis/Configuration/SonarLintConfigurationXmlSerializer.cs index 37ff4a0973..acd06133ed 100644 --- a/src/Integration/CSharpVB/SonarLintConfigXmlSerializer.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/SonarLintConfigurationXmlSerializer.cs @@ -25,12 +25,7 @@ using System.Xml.Serialization; using SonarLint.VisualStudio.Core.CSharpVB; -namespace SonarLint.VisualStudio.Integration.CSharpVB; - -internal interface ISonarLintConfigurationXmlSerializer -{ - string Serialize(SonarLintConfiguration configuration); -} +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; [Export(typeof(ISonarLintConfigurationXmlSerializer))] [PartCreationPolicy(CreationPolicy.Shared)] diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/SonarLintXmlConfigurationFile.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/SonarLintXmlConfigurationFile.cs new file mode 100644 index 0000000000..82e5555283 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/SonarLintXmlConfigurationFile.cs @@ -0,0 +1,41 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +internal class SonarLintXmlConfigurationFile : AdditionalText +{ + private readonly SourceText sourceText; + + public override string Path { get; } + + public string FileName { get; } = "SonarLint.xml"; + + public SonarLintXmlConfigurationFile(string baseDirectory, string content) + { + Path = System.IO.Path.Combine(baseDirectory, FileName); + sourceText = SourceText.From(content); + } + + public override SourceText GetText(CancellationToken cancellationToken = default) => sourceText; +} diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/SonarLintXmlProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/SonarLintXmlProvider.cs new file mode 100644 index 0000000000..532b5b2a2a --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/SonarLintXmlProvider.cs @@ -0,0 +1,50 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using System.IO; +using SonarLint.VisualStudio.Core.CSharpVB; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +[Export(typeof(ISonarLintXmlProvider))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +internal class SonarLintXmlProvider(ISonarLintConfigurationXmlSerializer sonarLintConfigurationXmlSerializer) : ISonarLintXmlProvider +{ + public SonarLintXmlConfigurationFile Create(RoslynAnalysisProfile analysisProfile) + { + var sonarLintConfiguration = new SonarLintConfiguration + { + Settings = ConvertDictionary(analysisProfile.AnalysisProperties), + Rules = analysisProfile + .Rules + .Where(x => x.IsActive) + .Select(x => new SonarLintRule { Key = x.RuleId.RuleKey, Parameters = ConvertDictionary(x.Parameters) }) + .ToList() + }; + + return new SonarLintXmlConfigurationFile(Path.GetTempPath(), sonarLintConfigurationXmlSerializer.Serialize(sonarLintConfiguration)); + } + + private static List? ConvertDictionary(Dictionary? dictionary) => dictionary?.Select(ConvertKeyValuePair).ToList() ?? []; + + private static SonarLintKeyValuePair ConvertKeyValuePair(KeyValuePair x) => new() { Key = x.Key, Value = x.Value }; +} diff --git a/src/RoslynAnalyzerServer/Analysis/DiagnosticDuplicatesComparer.cs b/src/RoslynAnalyzerServer/Analysis/DiagnosticDuplicatesComparer.cs new file mode 100644 index 0000000000..79d4e710ab --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/DiagnosticDuplicatesComparer.cs @@ -0,0 +1,70 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +public class DiagnosticDuplicatesComparer : IEqualityComparer +{ + public static DiagnosticDuplicatesComparer Instance { get; } = new(); + + private DiagnosticDuplicatesComparer() + { + } + + public bool Equals(RoslynIssue? x, RoslynIssue? y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + if (x is null) + { + return false; + } + if (y is null) + { + return false; + } + + return x.RuleId == y.RuleId && LocationEquals(x.PrimaryLocation, y.PrimaryLocation); + } + + public int GetHashCode(RoslynIssue obj) + { + unchecked + { + var hc = obj.RuleId.GetHashCode(); + const int prime = 397; + hc = (hc * prime) ^ obj.PrimaryLocation.FileUri.GetHashCode(); + hc = (hc * prime) ^ obj.PrimaryLocation.TextRange.StartLine; + hc = (hc * prime) ^ obj.PrimaryLocation.TextRange.StartLineOffset; + hc = (hc * prime) ^ obj.PrimaryLocation.TextRange.EndLine; + hc = (hc * prime) ^ obj.PrimaryLocation.TextRange.EndLineOffset; + return hc; + } + } + + private static bool LocationEquals(RoslynIssueLocation xPrimaryLocation, RoslynIssueLocation yPrimaryLocation) => + xPrimaryLocation.FileUri == yPrimaryLocation.FileUri && + xPrimaryLocation.TextRange.StartLine == yPrimaryLocation.TextRange.StartLine && + xPrimaryLocation.TextRange.EndLine == yPrimaryLocation.TextRange.EndLine && + xPrimaryLocation.TextRange.StartLineOffset == yPrimaryLocation.TextRange.StartLineOffset && + xPrimaryLocation.TextRange.EndLineOffset == yPrimaryLocation.TextRange.EndLineOffset; +} diff --git a/src/RoslynAnalyzerServer/Analysis/DiagnosticToRoslynIssueConverter.cs b/src/RoslynAnalyzerServer/Analysis/DiagnosticToRoslynIssueConverter.cs new file mode 100644 index 0000000000..c4f3909775 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/DiagnosticToRoslynIssueConverter.cs @@ -0,0 +1,78 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using Microsoft.CodeAnalysis; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Analysis; +using SonarLint.VisualStudio.SLCore.Common.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +[Export(typeof(IDiagnosticToRoslynIssueConverter))] +[PartCreationPolicy(CreationPolicy.Shared)] +public class DiagnosticToRoslynIssueConverter : IDiagnosticToRoslynIssueConverter +{ + public RoslynIssue ConvertToSonarDiagnostic(Diagnostic diagnostic, List quickFixes, Language language) => + new(SonarCompositeRuleId.GetFullErrorCode(language.RepoInfo.Key, diagnostic.Id), + ConvertLocation(diagnostic.Location.GetMappedLineSpan(), diagnostic.GetMessage()), + ConvertSecondaryLocations(diagnostic), + quickFixes.Select(x => new RoslynIssueQuickFix(x.GetStorageValue())).ToList()); + + private static IReadOnlyList ConvertSecondaryLocations(Diagnostic diagnostic) + { + if (diagnostic.AdditionalLocations.Count == 0) + { + return []; + } + + return + [ + // this will need to be modified once multi-flow locations are supported by the dotnet analyzer + new(diagnostic + .AdditionalLocations + .Select((location, index) => + { + if (!diagnostic.Properties.TryGetValue(index.ToString(), out var title) || title is null) + { + title = string.Format(Resources.DefaultSecondaryLocationTitleTemplate, index); + } + return ConvertLocation(location.GetMappedLineSpan(), title); + }) + .ToList()) + ]; + } + + private static RoslynIssueLocation ConvertLocation(FileLinePositionSpan fileLinePositionSpan, string message) + { + var textRange = new RoslynIssueTextRange( + fileLinePositionSpan.StartLinePosition.Line + 1, // roslyn lines are 0-based, while we use 1-based + fileLinePositionSpan.EndLinePosition.Line + 1, // roslyn lines are 0-based, while we use 1-based + fileLinePositionSpan.StartLinePosition.Character, + fileLinePositionSpan.EndLinePosition.Character); + + var location = new RoslynIssueLocation( + message, + new FileUri(fileLinePositionSpan.Path), + textRange); + + return location; + } +} diff --git a/src/RoslynAnalyzerServer/Analysis/IDiagnosticToRoslynIssueConverter.cs b/src/RoslynAnalyzerServer/Analysis/IDiagnosticToRoslynIssueConverter.cs new file mode 100644 index 0000000000..be9db27cf6 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/IDiagnosticToRoslynIssueConverter.cs @@ -0,0 +1,30 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.CodeAnalysis; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Analysis; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +public interface IDiagnosticToRoslynIssueConverter +{ + RoslynIssue ConvertToSonarDiagnostic(Diagnostic diagnostic, List quickFixes, Language language); +} diff --git a/src/RoslynAnalyzerServer/Analysis/IRoslynAnalysisCommand.cs b/src/RoslynAnalyzerServer/Analysis/IRoslynAnalysisCommand.cs new file mode 100644 index 0000000000..89f9c3271c --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/IRoslynAnalysisCommand.cs @@ -0,0 +1,30 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +internal interface IRoslynAnalysisCommand +{ + Task> ExecuteAsync(IRoslynCompilationWithAnalyzersWrapper compilation, CancellationToken token); +} diff --git a/src/RoslynAnalyzerServer/Analysis/IRoslynAnalysisEngine.cs b/src/RoslynAnalyzerServer/Analysis/IRoslynAnalysisEngine.cs new file mode 100644 index 0000000000..41557dd2c3 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/IRoslynAnalysisEngine.cs @@ -0,0 +1,31 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Core; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +internal interface IRoslynAnalysisEngine +{ + Task> AnalyzeAsync( + List projectsAnalysis, + IReadOnlyDictionary sonarRoslynAnalysisConfigurations, + CancellationToken token); +} diff --git a/src/RoslynAnalyzerServer/Analysis/IRoslynCodeActionFactory.cs b/src/RoslynAnalyzerServer/Analysis/IRoslynCodeActionFactory.cs new file mode 100644 index 0000000000..c264d812f6 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/IRoslynCodeActionFactory.cs @@ -0,0 +1,35 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +internal interface IRoslynCodeActionFactory +{ + Task> GetCodeActionsAsync( + IReadOnlyCollection codeFixProviders, + Diagnostic diagnostic, + IRoslynDocumentWrapper document, + CancellationToken token); +} diff --git a/src/RoslynAnalyzerServer/Analysis/IRoslynProjectCompilationProvider.cs b/src/RoslynAnalyzerServer/Analysis/IRoslynProjectCompilationProvider.cs new file mode 100644 index 0000000000..bd3887cb62 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/IRoslynProjectCompilationProvider.cs @@ -0,0 +1,33 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +internal interface IRoslynProjectCompilationProvider +{ + Task GetProjectCompilationAsync( + IRoslynProjectWrapper project, + IReadOnlyDictionary sonarRoslynAnalysisConfigurations, + CancellationToken token); +} diff --git a/src/RoslynAnalyzerServer/Analysis/IRoslynQuickFixFactory.cs b/src/RoslynAnalyzerServer/Analysis/IRoslynQuickFixFactory.cs new file mode 100644 index 0000000000..b6a3b2a01d --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/IRoslynQuickFixFactory.cs @@ -0,0 +1,34 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.CodeAnalysis; +using SonarLint.VisualStudio.Core.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +internal interface IRoslynQuickFixFactory +{ + Task> CreateQuickFixesAsync( + Diagnostic diagnostic, + IRoslynSolutionWrapper solution, + RoslynAnalysisConfiguration analysisConfiguration, + CancellationToken token); +} diff --git a/src/RoslynAnalyzerServer/Analysis/IRoslynSolutionAnalysisCommandProvider.cs b/src/RoslynAnalyzerServer/Analysis/IRoslynSolutionAnalysisCommandProvider.cs new file mode 100644 index 0000000000..da38cb34ab --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/IRoslynSolutionAnalysisCommandProvider.cs @@ -0,0 +1,26 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +internal interface IRoslynSolutionAnalysisCommandProvider +{ + List GetAnalysisCommandsForCurrentSolution(string[] filePaths); +} diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynAnalysisConfiguration.cs b/src/RoslynAnalyzerServer/Analysis/RoslynAnalysisConfiguration.cs new file mode 100644 index 0000000000..e285ece8bc --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/RoslynAnalysisConfiguration.cs @@ -0,0 +1,33 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +internal record struct RoslynAnalysisConfiguration( + SonarLintXmlConfigurationFile SonarLintXml, + ImmutableDictionary DiagnosticOptions, + ImmutableArray Analyzers, + ImmutableDictionary> CodeFixProvidersByRuleKey); diff --git a/src/Infrastructure.VS/Roslyn/AnalyzerAssemblyLoaderFactory.cs b/src/RoslynAnalyzerServer/Analysis/RoslynCodeActionFactory.cs similarity index 55% rename from src/Infrastructure.VS/Roslyn/AnalyzerAssemblyLoaderFactory.cs rename to src/RoslynAnalyzerServer/Analysis/RoslynCodeActionFactory.cs index 9e46653107..15c4b006a4 100644 --- a/src/Infrastructure.VS/Roslyn/AnalyzerAssemblyLoaderFactory.cs +++ b/src/RoslynAnalyzerServer/Analysis/RoslynCodeActionFactory.cs @@ -20,38 +20,24 @@ using System.ComponentModel.Composition; using System.Diagnostics.CodeAnalysis; -using System.Reflection; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; -namespace SonarLint.VisualStudio.Infrastructure.VS.Roslyn; +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; -[Export(typeof(IAnalyzerAssemblyLoaderFactory))] +[Export(typeof(IRoslynCodeActionFactory))] [PartCreationPolicy(CreationPolicy.Shared)] -internal class AnalyzerAssemblyLoaderFactory : IAnalyzerAssemblyLoaderFactory +[ExcludeFromCodeCoverage] +internal class RoslynCodeActionFactory : IRoslynCodeActionFactory { - private AnalyzerAssemblyLoader analyzerAssemblyLoader; - - [ImportingConstructor] - public AnalyzerAssemblyLoaderFactory() - { - } - - public IAnalyzerAssemblyLoader Create() - { - analyzerAssemblyLoader ??= new AnalyzerAssemblyLoader(); - return analyzerAssemblyLoader; - } - - [ExcludeFromCodeCoverage] - private sealed class AnalyzerAssemblyLoader : IAnalyzerAssemblyLoader + public async Task> GetCodeActionsAsync(IReadOnlyCollection codeFixProviders, Diagnostic diagnostic, IRoslynDocumentWrapper document, CancellationToken token) { - public void AddDependencyLocation(string fullPath) - { - } - - public Assembly LoadFromPath(string fullPath) + var codeActions = new List(); + foreach (var codeFixProvider in codeFixProviders) { - return Assembly.LoadFrom(fullPath); + await codeFixProvider.RegisterCodeFixesAsync(new CodeFixContext(document.RoslynDocument, diagnostic, (c, _) => codeActions.Add(new RoslynCodeActionWrapper(c)), token)); } + return codeActions; } } diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynFileSemanticAnalysis.cs b/src/RoslynAnalyzerServer/Analysis/RoslynFileSemanticAnalysis.cs new file mode 100644 index 0000000000..d8666597af --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/RoslynFileSemanticAnalysis.cs @@ -0,0 +1,43 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +internal class RoslynFileSemanticAnalysis(string analysisFilePath, ILogger logger) : IRoslynAnalysisCommand +{ + public string AnalysisFilePath { get; } = analysisFilePath; + + public async Task> ExecuteAsync(IRoslynCompilationWithAnalyzersWrapper compilation, CancellationToken token) + { + var semanticModel = compilation.GetSemanticModel(AnalysisFilePath); + if (semanticModel == null) + { + logger.LogVerbose(Resources.AnalysisCommand_NoSemanticModel, AnalysisFilePath); + return ImmutableArray.Empty; + } + + return await compilation.GetAnalyzerSemanticDiagnosticsAsync(semanticModel, token); + } +} diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynFileSyntaxAnalysis.cs b/src/RoslynAnalyzerServer/Analysis/RoslynFileSyntaxAnalysis.cs new file mode 100644 index 0000000000..032febf914 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/RoslynFileSyntaxAnalysis.cs @@ -0,0 +1,43 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +internal class RoslynFileSyntaxAnalysis(string analysisFilePath, ILogger logger) : IRoslynAnalysisCommand +{ + public string AnalysisFilePath { get; } = analysisFilePath; + + public async Task> ExecuteAsync(IRoslynCompilationWithAnalyzersWrapper compilation, CancellationToken token) + { + var syntaxTree = compilation.GetSyntaxTree(AnalysisFilePath); + if (syntaxTree == null) + { + logger.LogVerbose(Resources.AnalysisCommand_NoSyntaxTree, AnalysisFilePath); + return ImmutableArray.Empty; + } + + return await compilation.GetAnalyzerSyntaxDiagnosticsAsync(syntaxTree, token); + } +} diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynIssue.cs b/src/RoslynAnalyzerServer/Analysis/RoslynIssue.cs new file mode 100644 index 0000000000..c43bb0f4f7 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/RoslynIssue.cs @@ -0,0 +1,66 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.SLCore.Common.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +public class RoslynIssue( + string ruleId, + RoslynIssueLocation primaryLocation, + IReadOnlyList? flows = null, + IReadOnlyList? quickFixes = null) +{ + private static readonly IReadOnlyList EmptyFlows = []; + + public string RuleId { get; } = ruleId; + public RoslynIssueLocation PrimaryLocation { get; } = primaryLocation ?? throw new ArgumentNullException(nameof(primaryLocation)); + public IReadOnlyList Flows { get; } = flows ?? EmptyFlows; + public IReadOnlyList QuickFixes { get; } = quickFixes ?? []; +} + +public class RoslynIssueQuickFix(string value) +{ + public string Value { get; } = value; +} + +public class RoslynIssueFlow(IReadOnlyList locations) +{ + public IReadOnlyList Locations { get; } = locations ?? throw new ArgumentNullException(nameof(locations)); +} + +public class RoslynIssueLocation(string message, FileUri fileUri, RoslynIssueTextRange textRange) +{ + public FileUri FileUri { get; } = fileUri; + public string Message { get; } = message; + public RoslynIssueTextRange TextRange { get; } = textRange; +} + +public class RoslynIssueTextRange( + int startLine, + int endLine, + int startLineOffset, + int endLineOffset) +{ + public int StartLine { get; } = startLine; + public int EndLine { get; } = endLine; + public int StartLineOffset { get; } = startLineOffset; + public int EndLineOffset { get; } = endLineOffset; +} diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynProjectAnalysisRequest.cs b/src/RoslynAnalyzerServer/Analysis/RoslynProjectAnalysisRequest.cs new file mode 100644 index 0000000000..646b7a2a18 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/RoslynProjectAnalysisRequest.cs @@ -0,0 +1,29 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +internal class RoslynProjectAnalysisRequest(IRoslynProjectWrapper project, IReadOnlyCollection analysisCommands) +{ + public IRoslynProjectWrapper Project { get; } = project; + public IReadOnlyCollection AnalysisCommands { get; } = analysisCommands; +} diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynProjectCompilationProvider.cs b/src/RoslynAnalyzerServer/Analysis/RoslynProjectCompilationProvider.cs new file mode 100644 index 0000000000..29c8db91cb --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/RoslynProjectCompilationProvider.cs @@ -0,0 +1,87 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using System.ComponentModel.Composition; +using System.IO; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +[Export(typeof(IRoslynProjectCompilationProvider))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +internal class RoslynProjectCompilationProvider(ILogger logger) : IRoslynProjectCompilationProvider +{ + private readonly ILogger analyzerExceptionLogger = logger.ForContext(Resources.RoslynLogContext, Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisAnalyzerExceptionLogContext); + + public async Task GetProjectCompilationAsync( + IRoslynProjectWrapper project, + IReadOnlyDictionary sonarRoslynAnalysisConfigurations, + CancellationToken token) + { + var compilation = await project.GetCompilationAsync(token); + + var analysisConfigurationForLanguage = sonarRoslynAnalysisConfigurations[compilation.Language]; + + return ApplyAnalyzersAndAdditionalFile( + ApplyDiagnosticOptions(compilation, analysisConfigurationForLanguage), + project, + analysisConfigurationForLanguage); + } + + private IRoslynCompilationWithAnalyzersWrapper ApplyAnalyzersAndAdditionalFile( + IRoslynCompilationWrapper compilation, + IRoslynProjectWrapper project, + RoslynAnalysisConfiguration analysisConfigurationForLanguage) + { + var additionalFiles = project.RoslynAnalyzerOptions.AdditionalFiles; + var analyzerOptions = project.RoslynAnalyzerOptions.WithAdditionalFiles(additionalFiles + .Where(x => Path.GetFileName(x.Path) != analysisConfigurationForLanguage.SonarLintXml.FileName) + .Concat([analysisConfigurationForLanguage.SonarLintXml]) + .ToImmutableArray()); + + var compilationWithAnalyzersOptions = new CompilationWithAnalyzersOptions( + analyzerOptions, + OnAnalyzerException, + true, + false, + false); + + return compilation + .WithAnalyzers(analysisConfigurationForLanguage.Analyzers, compilationWithAnalyzersOptions, analysisConfigurationForLanguage); + } + + private static IRoslynCompilationWrapper ApplyDiagnosticOptions( + IRoslynCompilationWrapper compilation, + RoslynAnalysisConfiguration analysisConfigurationForLanguage) + { + var compilationOptions = compilation.RoslynCompilationOptions.WithSpecificDiagnosticOptions(analysisConfigurationForLanguage.DiagnosticOptions); + return compilation.WithOptions(compilationOptions); + } + + private void OnAnalyzerException(Exception arg1, DiagnosticAnalyzer arg2, Diagnostic arg3) => + analyzerExceptionLogger.LogVerbose( + new MessageLevelContext { VerboseContext = [arg2.GetType().Name, arg3.Id] }, + arg1.ToString()); +} diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynQuickFixFactory.cs b/src/RoslynAnalyzerServer/Analysis/RoslynQuickFixFactory.cs new file mode 100644 index 0000000000..aba7b492d7 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/RoslynQuickFixFactory.cs @@ -0,0 +1,56 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using Microsoft.CodeAnalysis; +using SonarLint.VisualStudio.Core.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +[Export(typeof(IRoslynQuickFixFactory))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method:ImportingConstructor] +internal class RoslynQuickFixFactory(IRoslynWorkspaceWrapper workspace, IRoslynCodeActionFactory roslynCodeActionFactory, IRoslynQuickFixStorageWriter quickFixStorage) + : IRoslynQuickFixFactory +{ + public async Task> CreateQuickFixesAsync( + Diagnostic diagnostic, + IRoslynSolutionWrapper solution, + RoslynAnalysisConfiguration analysisConfiguration, + CancellationToken token) + { + var quickFixes = new List(); + + if (analysisConfiguration.CodeFixProvidersByRuleKey.TryGetValue(diagnostic.Id, out var availableCodeFixProviders) + && solution.GetDocument(diagnostic.Location.SourceTree) is {} document + && await roslynCodeActionFactory.GetCodeActionsAsync(availableCodeFixProviders, diagnostic, document, token) is {} codeActions) + { + foreach (var codeAction in codeActions) + { + var id = Guid.NewGuid(); + quickFixStorage.Add(id, new RoslynQuickFixApplicationImpl(workspace, solution, codeAction)); + quickFixes.Add(new RoslynQuickFix(id)); + } + } + + return quickFixes; + } +} diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynSolutionAnalysisCommandProvider.cs b/src/RoslynAnalyzerServer/Analysis/RoslynSolutionAnalysisCommandProvider.cs new file mode 100644 index 0000000000..b3eef5b195 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/RoslynSolutionAnalysisCommandProvider.cs @@ -0,0 +1,82 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +[Export(typeof(IRoslynSolutionAnalysisCommandProvider))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +internal class RoslynSolutionAnalysisCommandProvider( + IRoslynWorkspaceWrapper roslynWorkspaceWrapper, + ILogger logger) : IRoslynSolutionAnalysisCommandProvider +{ + private readonly ILogger logger = logger.ForContext(Resources.RoslynLogContext, Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisConfigurationLogContext); + + public List GetAnalysisCommandsForCurrentSolution(string[] filePaths) + { + var result = new List(); + + var solution = roslynWorkspaceWrapper.GetCurrentSolution(); + + foreach (var project in solution.Projects) + { + if (!project.SupportsCompilation) + { + logger.LogVerbose(Resources.AnalysisCommandProvider_NoCompilationForProject, project.Name); + continue; + } + + var commands = GetCompilationCommandsForProject(filePaths, project); + + if (commands.Any()) + { + result.Add(new RoslynProjectAnalysisRequest(project, commands)); + } + } + + if (!result.Any()) + { + logger.WriteLine(Resources.AnalysisCommandProvider_NoProjects); + } + + return result; + } + + private List GetCompilationCommandsForProject(string[] filePaths, IRoslynProjectWrapper project) + { + var commands = new List(); + + foreach (var filePath in filePaths) + { + if (!project.ContainsDocument(filePath, out var analysisFilePath)) + { + continue; + } + + commands.Add(new RoslynFileSyntaxAnalysis(analysisFilePath, logger)); + commands.Add(new RoslynFileSemanticAnalysis(analysisFilePath, logger)); + } + return commands; + } +} diff --git a/src/RoslynAnalyzerServer/Analysis/SequentialRoslynAnalysisEngine.cs b/src/RoslynAnalyzerServer/Analysis/SequentialRoslynAnalysisEngine.cs new file mode 100644 index 0000000000..43a95de332 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/SequentialRoslynAnalysisEngine.cs @@ -0,0 +1,73 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using System.IO; +using SonarLint.VisualStudio.Core; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +[Export(typeof(IRoslynAnalysisEngine))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +internal class SequentialRoslynAnalysisEngine( + IDiagnosticToRoslynIssueConverter issueConverter, + IRoslynProjectCompilationProvider projectCompilationProvider, + IRoslynQuickFixFactory quickFixFactory, + ILogger logger) : IRoslynAnalysisEngine +{ + private readonly ILogger logger = logger.ForContext(Resources.RoslynLogContext, Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisEngineLogContext); + + public async Task> AnalyzeAsync( + List projectsAnalysis, + IReadOnlyDictionary sonarRoslynAnalysisConfigurations, + CancellationToken token) + { + var uniqueDiagnostics = new HashSet(DiagnosticDuplicatesComparer.Instance); + foreach (var projectAnalysisCommands in projectsAnalysis) + { + var compilationWithAnalyzers = await projectCompilationProvider.GetProjectCompilationAsync(projectAnalysisCommands.Project, sonarRoslynAnalysisConfigurations, token); + + // todo SLVS-2467 issue streaming + foreach (var analysisCommand in projectAnalysisCommands.AnalysisCommands) + { + var diagnostics = await analysisCommand.ExecuteAsync(compilationWithAnalyzers, token); + + foreach (var diagnostic in diagnostics) + { + var quickFixes = await quickFixFactory.CreateQuickFixesAsync( + diagnostic, + projectAnalysisCommands.Project.Solution, + compilationWithAnalyzers.AnalysisConfiguration, + token); + + var roslynIssue = issueConverter.ConvertToSonarDiagnostic(diagnostic, quickFixes, compilationWithAnalyzers.Language); + // todo SLVS-2468 improve issue merging + if (!uniqueDiagnostics.Add(roslynIssue)) + { + logger.LogVerbose(Resources.AnalysisEngine_DuplicateDiagnostic, roslynIssue.RuleId, roslynIssue.PrimaryLocation.FileUri.LocalPath, roslynIssue.PrimaryLocation.TextRange.StartLine); + } + } + } + } + + return uniqueDiagnostics; + } +} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/Logging/EnableAllLoggerSettingsProvider.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCodeActionWrapper.cs similarity index 72% rename from src/Roslyn.Suppressions/Roslyn.Suppressions/Logging/EnableAllLoggerSettingsProvider.cs rename to src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCodeActionWrapper.cs index a58f8a47c2..c5e3938696 100644 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/Logging/EnableAllLoggerSettingsProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCodeActionWrapper.cs @@ -18,14 +18,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Diagnostics.CodeAnalysis; -using SonarLint.VisualStudio.Core.Logging; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.CodeActions; -namespace SonarLint.VisualStudio.Roslyn.Suppressions; +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; -[ExcludeFromCodeCoverage] -internal class EnableAllLoggerSettingsProvider : ILoggerSettingsProvider +internal interface IRoslynCodeActionWrapper { - public bool IsVerboseEnabled => true; - public bool IsThreadIdEnabled => true; + string Title { get; } + + Task> GetOperationsAsync(CancellationToken cancellationToken); } diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCompilationWithAnalyzersWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCompilationWithAnalyzersWrapper.cs new file mode 100644 index 0000000000..3f15e480c9 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCompilationWithAnalyzersWrapper.cs @@ -0,0 +1,37 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using SonarLint.VisualStudio.Core; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +internal interface IRoslynCompilationWithAnalyzersWrapper +{ + RoslynLanguage Language { get; } + RoslynAnalysisConfiguration AnalysisConfiguration { get; } + SyntaxTree? GetSyntaxTree(string filePath); + + SemanticModel? GetSemanticModel(string filePath); + + Task> GetAnalyzerSyntaxDiagnosticsAsync(SyntaxTree syntaxTree, CancellationToken token); + Task> GetAnalyzerSemanticDiagnosticsAsync(SemanticModel semanticModel, CancellationToken token); +} diff --git a/src/Infrastructure.VS/Roslyn/AnalyzerArrayComparer.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCompilationWrapper.cs similarity index 57% rename from src/Infrastructure.VS/Roslyn/AnalyzerArrayComparer.cs rename to src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCompilationWrapper.cs index d29069174b..b3a2fc9d0f 100644 --- a/src/Infrastructure.VS/Roslyn/AnalyzerArrayComparer.cs +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCompilationWrapper.cs @@ -19,32 +19,21 @@ */ using System.Collections.Immutable; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; +using SonarLint.VisualStudio.Core; -namespace SonarLint.VisualStudio.Infrastructure.VS.Roslyn; +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; -internal class AnalyzerArrayComparer : IEqualityComparer?> +internal interface IRoslynCompilationWrapper { - public static AnalyzerArrayComparer Instance { get; } = new(); + CompilationOptions RoslynCompilationOptions { get; } + RoslynLanguage Language { get; } - private AnalyzerArrayComparer() - { - } + IRoslynCompilationWrapper WithOptions(CompilationOptions withSpecificDiagnosticOptions); - public bool Equals(ImmutableArray? x, ImmutableArray? y) - { - if (x is null && y is null) - { - return true; - } - - if (x is null || y is null) - { - return false; - } - - return x.Value.SequenceEqual(y.Value); - } - - public int GetHashCode(ImmutableArray? obj) => obj.GetHashCode(); + IRoslynCompilationWithAnalyzersWrapper WithAnalyzers( + ImmutableArray analyzers, + CompilationWithAnalyzersOptions compilationWithAnalyzersOptions, + RoslynAnalysisConfiguration analysisConfiguration); } diff --git a/src/Infrastructure.VS/Roslyn/IAnalyzerAssemblyLoaderFactory.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynDocumentWrapper.cs similarity index 85% rename from src/Infrastructure.VS/Roslyn/IAnalyzerAssemblyLoaderFactory.cs rename to src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynDocumentWrapper.cs index e65b9396aa..679bf891de 100644 --- a/src/Infrastructure.VS/Roslyn/IAnalyzerAssemblyLoaderFactory.cs +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynDocumentWrapper.cs @@ -20,9 +20,9 @@ using Microsoft.CodeAnalysis; -namespace SonarLint.VisualStudio.Infrastructure.VS.Roslyn; +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; -public interface IAnalyzerAssemblyLoaderFactory +internal interface IRoslynDocumentWrapper { - IAnalyzerAssemblyLoader Create(); + Document RoslynDocument { get; } } diff --git a/src/Infrastructure.VS/Roslyn/IEnterpriseRoslynAnalyzerProvider.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynProjectWrapper.cs similarity index 66% rename from src/Infrastructure.VS/Roslyn/IEnterpriseRoslynAnalyzerProvider.cs rename to src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynProjectWrapper.cs index 2b430e46a2..db8866853b 100644 --- a/src/Infrastructure.VS/Roslyn/IEnterpriseRoslynAnalyzerProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynProjectWrapper.cs @@ -18,17 +18,21 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; -using SonarLint.VisualStudio.Core.Binding; -namespace SonarLint.VisualStudio.Infrastructure.VS.Roslyn; +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; -public interface IEnterpriseRoslynAnalyzerProvider +internal interface IRoslynProjectWrapper { - /// - /// Returns SonarAnalyzer.CSharp & SonarAnalyzer.VisualBasic analyzer DLLs that are downloaded from the server for the current binding - /// - Task?> GetEnterpriseOrNullAsync(string configurationScopeId); + string Name { get; } + bool SupportsCompilation { get; } + AnalyzerOptions RoslynAnalyzerOptions { get; } + IRoslynSolutionWrapper Solution { get; } + + bool ContainsDocument( + string filePath, + [NotNullWhen(true)]out string? analysisFilePath); + + Task GetCompilationAsync(CancellationToken token); } diff --git a/src/SonarQube.Client/Api/IGetRulesRequest.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynSolutionWrapper.cs similarity index 74% rename from src/SonarQube.Client/Api/IGetRulesRequest.cs rename to src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynSolutionWrapper.cs index 114d1f6ef7..ad5cdfb1c7 100644 --- a/src/SonarQube.Client/Api/IGetRulesRequest.cs +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynSolutionWrapper.cs @@ -18,17 +18,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using SonarQube.Client.Models; -using SonarQube.Client.Requests; +using Microsoft.CodeAnalysis; -namespace SonarQube.Client.Api -{ - public interface IGetRulesRequest : IPagedRequest - { - bool? IsActive { get; set; } +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; - string QualityProfileKey { get; set; } +internal interface IRoslynSolutionWrapper +{ + IEnumerable Projects { get; } + Solution RoslynSolution { get; } - string RuleKey { get; set; } - } + IRoslynDocumentWrapper? GetDocument(SyntaxTree? tree); } diff --git a/src/SonarQube.Client/Api/Common/ServerComponent.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynWorkspaceWrapper.cs similarity index 65% rename from src/SonarQube.Client/Api/Common/ServerComponent.cs rename to src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynWorkspaceWrapper.cs index fa776b3de8..9c0682aa25 100644 --- a/src/SonarQube.Client/Api/Common/ServerComponent.cs +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynWorkspaceWrapper.cs @@ -18,24 +18,17 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using Newtonsoft.Json; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; -namespace SonarQube.Client.Api.Common -{ - internal sealed class ServerComponent - { - [JsonProperty("key")] - public string Key { get; set; } - - [JsonProperty("qualifier")] - public string Qualifier { get; set; } +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; - [JsonProperty("path")] - public string Path { get; set; } +internal interface IRoslynWorkspaceWrapper : IDisposable +{ + IRoslynSolutionWrapper GetCurrentSolution(); - public bool IsFile - { - get { return Qualifier == "FIL"; } - } - } + Task ApplyOrMergeChangesAsync( + IRoslynSolutionWrapper originalSolution, + Microsoft.CodeAnalysis.CodeActions.ApplyChangesOperation operation, + CancellationToken cancellationToken); } diff --git a/src/SonarQube.Client/Models/ServerSentEvents/ServerContract/ISqServerEvent.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/IWorkspaceChangeIndicator.cs similarity index 54% rename from src/SonarQube.Client/Models/ServerSentEvents/ServerContract/ISqServerEvent.cs rename to src/RoslynAnalyzerServer/Analysis/Wrappers/IWorkspaceChangeIndicator.cs index 3599e2d69f..f3af6edc53 100644 --- a/src/SonarQube.Client/Models/ServerSentEvents/ServerContract/ISqServerEvent.cs +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/IWorkspaceChangeIndicator.cs @@ -18,30 +18,27 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -namespace SonarQube.Client.Models.ServerSentEvents.ServerContract +using Microsoft.CodeAnalysis; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +/// +/// Indicates whether workspace changes are critical and require reanalysis +/// +internal interface IWorkspaceChangeIndicator { /// - /// Represents the raw event information coming from the server + /// Checks if solution change event is not critical. Does not guarantee that it is critical if returns false. /// - internal interface ISqServerEvent - { - string Type { get; } + bool IsChangeKindTrivial(WorkspaceChangeKind kind); - /// - /// Json-serialized event data - /// - string Data { get; } - } - - internal class SqServerEvent : ISqServerEvent - { - public SqServerEvent(string type, string data) - { - Type = type; - Data = data; - } + /// + /// Checks if solution changes are critical and require reanalysis + /// + bool SolutionChangedCritically(SolutionChanges solutionChanges); - public string Type { get; } - public string Data { get; } - } + /// + /// Checks if project changes are critical and require reanalysis + /// + bool ProjectChangedCritically(ProjectChanges changedProject); } diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCodeActionWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCodeActionWrapper.cs new file mode 100644 index 0000000000..5d07c8aeef --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCodeActionWrapper.cs @@ -0,0 +1,33 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +[ExcludeFromCodeCoverage] +internal class RoslynCodeActionWrapper(CodeAction codeAction) : IRoslynCodeActionWrapper +{ + public string Title => codeAction.Title; + + public Task> GetOperationsAsync(CancellationToken cancellationToken) => codeAction.GetOperationsAsync(cancellationToken); +} diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCompilationWithAnalyzersWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCompilationWithAnalyzersWrapper.cs new file mode 100644 index 0000000000..ced955a43b --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCompilationWithAnalyzersWrapper.cs @@ -0,0 +1,44 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using SonarLint.VisualStudio.Core; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +[ExcludeFromCodeCoverage] // todo SLVS-2466 add roslyn 'integration' tests using AdHocWorkspace +internal class RoslynCompilationWithAnalyzersWrapper(CompilationWithAnalyzers compilation, RoslynAnalysisConfiguration analysisConfiguration, RoslynLanguage language) : IRoslynCompilationWithAnalyzersWrapper +{ + public RoslynLanguage Language { get; } = language; + public RoslynAnalysisConfiguration AnalysisConfiguration { get; } = analysisConfiguration; + + public SyntaxTree? GetSyntaxTree(string filePath) => compilation.Compilation.SyntaxTrees.SingleOrDefault(x => filePath.Equals(x.FilePath)); + + public SemanticModel? GetSemanticModel(string filePath) => GetSyntaxTree(filePath) is {} syntaxTree ? compilation.Compilation.GetSemanticModel(syntaxTree) : null; + + public Task> GetAnalyzerSyntaxDiagnosticsAsync(SyntaxTree syntaxTree, CancellationToken token) => + compilation.GetAnalyzerSyntaxDiagnosticsAsync(syntaxTree, token); + + public Task> GetAnalyzerSemanticDiagnosticsAsync(SemanticModel semanticModel, CancellationToken token) => + compilation.GetAnalyzerSemanticDiagnosticsAsync(semanticModel, null, token); +} diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCompilationWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCompilationWrapper.cs new file mode 100644 index 0000000000..b80ba9fde5 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCompilationWrapper.cs @@ -0,0 +1,49 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using SonarLint.VisualStudio.Core; +using Languages = SonarLint.VisualStudio.Core.Language; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +[ExcludeFromCodeCoverage] // todo SLVS-2466 add roslyn 'integration' tests using AdHocWorkspace +internal class RoslynCompilationWrapper(Compilation roslynCompilation) : IRoslynCompilationWrapper +{ + public CompilationOptions RoslynCompilationOptions => roslynCompilation.Options; + public RoslynLanguage Language { get; } = roslynCompilation.Language switch + { + LanguageNames.CSharp => Languages.CSharp, + LanguageNames.VisualBasic => Languages.VBNET, + _ => throw new ArgumentOutOfRangeException(nameof(roslynCompilation)), + }; + + public IRoslynCompilationWrapper WithOptions(CompilationOptions withSpecificDiagnosticOptions) => + new RoslynCompilationWrapper(roslynCompilation.WithOptions(withSpecificDiagnosticOptions)); + + public IRoslynCompilationWithAnalyzersWrapper WithAnalyzers( + ImmutableArray analyzers, + CompilationWithAnalyzersOptions compilationWithAnalyzersOptions, + RoslynAnalysisConfiguration analysisConfiguration) => + new RoslynCompilationWithAnalyzersWrapper(roslynCompilation.WithAnalyzers(analyzers, compilationWithAnalyzersOptions), analysisConfiguration, Language); +} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/Logging/SystemDebugLoggerWriter.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynDocumentWrapper.cs similarity index 78% rename from src/Roslyn.Suppressions/Roslyn.Suppressions/Logging/SystemDebugLoggerWriter.cs rename to src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynDocumentWrapper.cs index 007098e710..f92efea206 100644 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/Logging/SystemDebugLoggerWriter.cs +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynDocumentWrapper.cs @@ -19,12 +19,12 @@ */ using System.Diagnostics.CodeAnalysis; -using SonarLint.VisualStudio.Core.Logging; +using Microsoft.CodeAnalysis; -namespace SonarLint.VisualStudio.Roslyn.Suppressions; +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; [ExcludeFromCodeCoverage] -internal class SystemDebugLoggerWriter : ILoggerWriter +internal class RoslynDocumentWrapper(Document roslynDocument) : IRoslynDocumentWrapper { - public void WriteLine(string message) => Debug.WriteLine(message); + public Document RoslynDocument { get; } = roslynDocument; } diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynProjectWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynProjectWrapper.cs new file mode 100644 index 0000000000..824d6cbf7d --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynProjectWrapper.cs @@ -0,0 +1,53 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +[ExcludeFromCodeCoverage] // todo SLVS-2466 add roslyn 'integration' tests using AdHocWorkspace +internal class RoslynProjectWrapper(Project project, IRoslynSolutionWrapper solution) : IRoslynProjectWrapper +{ + public string Name => project.Name; + public bool SupportsCompilation => project.SupportsCompilation; + public IRoslynSolutionWrapper Solution => solution; + public AnalyzerOptions RoslynAnalyzerOptions => project.AnalyzerOptions; + + public async Task GetCompilationAsync(CancellationToken token) => new RoslynCompilationWrapper((await project.GetCompilationAsync(token))!); + + public bool ContainsDocument( + string filePath, + [NotNullWhen(true)] out string? analysisFilePath) + { + analysisFilePath = project.Documents + .Select(document => document.FilePath) + .Where(path => path != null) + .FirstOrDefault(candidatePath => + candidatePath!.Equals(filePath) || IsAssociatedGeneratedFile(filePath, candidatePath)); + + return analysisFilePath != null; + } + + // cshtml razor files are converted into .\file.cshtml..g.cs OR .\file.vbhtml..g.vb files when included in the compilation + private static bool IsAssociatedGeneratedFile(string razorFilePath, string candidateDocumentPath) => + candidateDocumentPath.StartsWith(razorFilePath) && (candidateDocumentPath.EndsWith(".g.cs") || candidateDocumentPath.EndsWith(".g.vb")); +} diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynSolutionWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynSolutionWrapper.cs new file mode 100644 index 0000000000..68a003cc5b --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynSolutionWrapper.cs @@ -0,0 +1,39 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +[ExcludeFromCodeCoverage] // todo SLVS-2466 add roslyn 'integration' tests using AdHocWorkspace +internal class RoslynSolutionWrapper : IRoslynSolutionWrapper +{ + public RoslynSolutionWrapper(Solution solution) + { + RoslynSolution = solution; + Projects = solution.Projects.Select(x => new RoslynProjectWrapper(x, this)); + } + + public IEnumerable Projects { get; } + public Solution RoslynSolution { get; } + + public IRoslynDocumentWrapper? GetDocument(SyntaxTree? tree) => RoslynSolution.GetDocument(tree) is {} roslynDocument ? new RoslynDocumentWrapper(roslynDocument) : null; +} diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynWorkspaceWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynWorkspaceWrapper.cs new file mode 100644 index 0000000000..8c02b76ad6 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynWorkspaceWrapper.cs @@ -0,0 +1,96 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.VisualStudio.LanguageServices; +using Microsoft.VisualStudio.Threading; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Analysis; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +[ExcludeFromCodeCoverage] // todo SLVS-2466 add roslyn 'integration' tests using AdHocWorkspace +[Export(typeof(IRoslynWorkspaceWrapper))] +[PartCreationPolicy(CreationPolicy.Shared)] +internal sealed class RoslynWorkspaceWrapper : IRoslynWorkspaceWrapper +{ + private readonly ILogger quickFixApplicationLogger; + private readonly Workspace workspace; + private readonly IAnalysisRequester analysisRequester; + private readonly IThreadHandling threadHandling; + private readonly IWorkspaceChangeIndicator workspaceChangeIndicator; + private bool disposed; + + [method: ImportingConstructor] + public RoslynWorkspaceWrapper( + [Import(typeof(VisualStudioWorkspace))] + Workspace workspace, + IWorkspaceChangeIndicator workspaceChangeIndicator, + IAnalysisRequester analysisRequester, + ILogger logger, + IThreadHandling threadHandling) + { + this.workspace = workspace; + this.analysisRequester = analysisRequester; + this.threadHandling = threadHandling; + this.workspaceChangeIndicator = workspaceChangeIndicator; + workspace.WorkspaceChanged += WorkspaceOnWorkspaceChanged; + quickFixApplicationLogger = logger.ForContext(Resources.RoslynLogContext, Resources.RoslynQuickFixLogContext); + } + + public IRoslynSolutionWrapper GetCurrentSolution() => new RoslynSolutionWrapper(workspace.CurrentSolution); + + public Task ApplyOrMergeChangesAsync(IRoslynSolutionWrapper originalSolution, Microsoft.CodeAnalysis.CodeActions.ApplyChangesOperation operation, CancellationToken cancellationToken) => + ApplyChangesOperation.ApplyOrMergeChangesAsync(workspace, originalSolution.RoslynSolution, operation.ChangedSolution, quickFixApplicationLogger, workspaceChangeIndicator, cancellationToken); + + // todo SLVS-2466 add roslyn 'integration' tests using AdHocWorkspace + private void WorkspaceOnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e) + { + if (workspaceChangeIndicator.IsChangeKindTrivial(e.Kind)) + { + return; + } + + threadHandling.RunOnBackgroundThread(() => + { + var solutionChanges = e.NewSolution.GetChanges(e.OldSolution); + + if (workspaceChangeIndicator.SolutionChangedCritically(solutionChanges) + || solutionChanges.GetProjectChanges().Any(changedProject => workspaceChangeIndicator.ProjectChangedCritically(changedProject))) + { + analysisRequester.RequestAnalysis(); + } + }).Forget(); + } + + public void Dispose() + { + if (disposed) + { + return; + } + + workspace.WorkspaceChanged -= WorkspaceOnWorkspaceChanged; + disposed = true; + } +} diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/WorkspaceChangeIndicator.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/WorkspaceChangeIndicator.cs new file mode 100644 index 0000000000..242fe9557d --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/WorkspaceChangeIndicator.cs @@ -0,0 +1,87 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using System.Linq; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +[ExcludeFromCodeCoverage] // todo SLVS-2466 add roslyn 'integration' tests using AdHocWorkspace +[Export(typeof(IWorkspaceChangeIndicator))] +[PartCreationPolicy(CreationPolicy.Shared)] +internal class WorkspaceChangeIndicator : IWorkspaceChangeIndicator +{ + private readonly HashSet trivialChanges = + [ + // Currently changes in other files should not affect too many quickfixes + // There is an opportunity for improvement: + // this group needs to be removed, and a mechanism that either re-analyzes on apply fail, + // or a background analysis job, need to be added + // todo https://sonarsource.atlassian.net/browse/SLVS-2612 + WorkspaceChangeKind.DocumentAdded, + WorkspaceChangeKind.DocumentRemoved, + WorkspaceChangeKind.DocumentReloaded, + WorkspaceChangeKind.DocumentChanged, + WorkspaceChangeKind.DocumentInfoChanged, + WorkspaceChangeKind.AdditionalDocumentAdded, + WorkspaceChangeKind.AdditionalDocumentRemoved, + WorkspaceChangeKind.AdditionalDocumentReloaded, + WorkspaceChangeKind.AdditionalDocumentChanged, + + // we don't care about other analyzers + WorkspaceChangeKind.AnalyzerConfigDocumentAdded, + WorkspaceChangeKind.AnalyzerConfigDocumentRemoved, + WorkspaceChangeKind.AnalyzerConfigDocumentReloaded, + WorkspaceChangeKind.AnalyzerConfigDocumentChanged, + ]; + + public bool IsChangeKindTrivial(WorkspaceChangeKind kind) => trivialChanges.Contains(kind); + + public bool SolutionChangedCritically(SolutionChanges solutionChanges) => + solutionChanges.GetAddedProjects().Any() || + solutionChanges.GetRemovedProjects().Any(); + + // We don't care about other analyzers. This is preserved for documentation's sake, as this code was originally copied from roslyn's github + // solutionChanges.GetAddedAnalyzerReferences().Any() || + // solutionChanges.GetRemovedAnalyzerReferences().Any(); + + public bool ProjectChangedCritically(ProjectChanges changedProject) => + changedProject.GetAddedMetadataReferences().Any() || + changedProject.GetAddedProjectReferences().Any() || + changedProject.GetRemovedMetadataReferences().Any() || + changedProject.GetRemovedProjectReferences().Any(); + + // todo https://sonarsource.atlassian.net/browse/SLVS-2612 + // Currently changes in other files should not affect too many quickfixes, see comment above. + // changedProject.GetAddedDocuments().Any() || + // changedProject.GetRemovedDocuments().Any() || + // changedProject.GetAddedAdditionalDocuments().Any() || + // changedProject.GetRemovedAdditionalDocuments().Any() || + + // We don't care about configs as we get rules configuration from SLCore. This is preserved for documentation's sake, as this code was originally copied from roslyn's github + // changedProject.GetAddedAnalyzerConfigDocuments().Any() || + // changedProject.GetRemovedAnalyzerConfigDocuments().Any() || + + // We don't care about other analyzers or suppressors. This is preserved for documentation's sake, as this code was originally copied from roslyn's github + // changedProject.GetAddedAnalyzerReferences().Any() || + // changedProject.GetRemovedAnalyzerReferences().Any() || +} diff --git a/src/RoslynAnalyzerServer/ApplyChangesOperation.Roslyn.cs b/src/RoslynAnalyzerServer/ApplyChangesOperation.Roslyn.cs new file mode 100644 index 0000000000..ed7871a57b --- /dev/null +++ b/src/RoslynAnalyzerServer/ApplyChangesOperation.Roslyn.cs @@ -0,0 +1,139 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; +using Document = Microsoft.CodeAnalysis.Document; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer; + +// This class was adapted from https://github.com/dotnet/roslyn/blob/75e79dace86b274327a1afe479228d82a06051a4/src/Workspaces/Core/Portable/CodeActions/Operations/ApplyChangesOperation.cs#L46 +[ExcludeFromCodeCoverage] +public static class ApplyChangesOperation +{ + internal static async Task ApplyOrMergeChangesAsync( + Workspace workspace, + Solution originalSolution, + Solution changedSolution, + ILogger logger, + IWorkspaceChangeIndicator workspaceChangeIndicator, + CancellationToken cancellationToken) + { + var currentSolution = workspace.CurrentSolution; + + // if there was no intermediary edit, just apply the change fully. + if (workspace.TryApplyChanges(changedSolution)) + { + return true; + } + + // Otherwise, we need to see what changes were actually made and see if we can apply them. The general rules are: + // + // 1. we only support text changes when doing merges. Any other changes to projects/documents are not + // supported because it's very unclear what impact they may have wrt other workspace updates that have + // already happened. + // + // 2. For text changes, we only support it if the current text of the document we're changing itself has not + // changed. This means we can merge in edits if there were changes to unrelated files, but not if there + // are changes to the current file. + + var solutionChanges = changedSolution.GetChanges(originalSolution); + + if (workspaceChangeIndicator.SolutionChangedCritically(solutionChanges)) + { + logger.LogVerbose(Resources.ApplyChangesOperation_SolutionChanged); + return false; + } + + // Take the actual current solution the workspace is pointing to and fork it with just the text changes the + // code action wanted to make. Then apply that fork back into the workspace. + var forkedSolution = currentSolution; + + foreach (var changedProject in solutionChanges.GetProjectChanges()) + { + // We only support text changes. If we see any other changes to this project, bail out immediately. + if (workspaceChangeIndicator.ProjectChangedCritically(changedProject)) + { + logger.LogVerbose(Resources.ApplyChangesOperation_ProjectChanged, changedProject.NewProject.Name); + return false; + } + + // We have to at least have some changed document + var changedDocuments = GetChangedDocuments(changedProject); + + if (changedDocuments.Length == 0) + { + return false; + } + + foreach (var documentId in changedDocuments) + { + if (!GetDocuments(changedProject, documentId, out var originalDocument, out var changedDocument)) + { + return false; + } + + // it has to be a text change the operation wants to make. If the operation is making some other + // sort of change, we can't merge this operation in. + if (await changedDocument.GetTextVersionAsync(cancellationToken) == await originalDocument.GetTextVersionAsync(cancellationToken)) + { + return false; + } + + // If the document has gone away, we definitely cannot apply a text change to it. + var currentDocument = currentSolution.GetDocument(documentId); + if (currentDocument is null) + { + return false; + } + + // If the file contents changed in the current workspace, then we can't apply this change to it. + if (await originalDocument.GetTextVersionAsync(cancellationToken) != await currentDocument.GetTextVersionAsync(cancellationToken)) + { + return false; + } + + forkedSolution = forkedSolution.WithDocumentText(documentId, await changedDocument.GetTextAsync(cancellationToken)); + } + } + + return workspace.TryApplyChanges(forkedSolution); + } + + private static ImmutableArray GetChangedDocuments(ProjectChanges changedProject) + { + var changedDocuments = changedProject.GetChangedDocuments() + .Concat(changedProject.GetChangedAdditionalDocuments()) + .Concat(changedProject.GetChangedAnalyzerConfigDocuments()).ToImmutableArray(); + return changedDocuments; + } + + private static bool GetDocuments( + ProjectChanges changedProject, + DocumentId documentId, + [NotNullWhen(true)]out Document? originalDocument, + [NotNullWhen(true)]out Document? changedDocument) + { + originalDocument = changedProject.OldProject.Solution.GetDocument(documentId); + + if (originalDocument == null) + { + Debug.Fail("Original document not found"); + changedDocument = null; + return false; + } + + changedDocument = changedProject.NewProject.Solution.GetDocument(documentId); + + if (changedDocument == null) + { + Debug.Fail("Changed document not found"); + return false; + } + return true; + } +} diff --git a/src/SonarQube.Client/Api/Common/ServerIssueTextRange.cs b/src/RoslynAnalyzerServer/Http/Adapters/IHttpListenerContext.cs similarity index 63% rename from src/SonarQube.Client/Api/Common/ServerIssueTextRange.cs rename to src/RoslynAnalyzerServer/Http/Adapters/IHttpListenerContext.cs index 30a71adeab..123dbde8b0 100644 --- a/src/SonarQube.Client/Api/Common/ServerIssueTextRange.cs +++ b/src/RoslynAnalyzerServer/Http/Adapters/IHttpListenerContext.cs @@ -18,22 +18,18 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using Newtonsoft.Json; +using System.Net; -namespace SonarQube.Client.Api.Common -{ - internal sealed class ServerIssueTextRange - { - [JsonProperty("startLine")] - public int StartLine { get; set; } - - [JsonProperty("endLine")] - public int EndLine { get; set; } +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Adapters; - [JsonProperty("startOffset")] - public int StartOffset { get; set; } +public interface IHttpListenerContext +{ + IHttpListenerRequest Request { get; } + IHttpListenerResponse Response { get; } +} - [JsonProperty("endOffset")] - public int EndOffset { get; set; } - } +public class HttpListenerContextAdapter(HttpListenerContext context) : IHttpListenerContext +{ + public IHttpListenerRequest Request => new HttpListenerRequestAdapter(context.Request); + public IHttpListenerResponse Response => new HttpListenerResponseAdapter(context.Response); } diff --git a/src/RoslynAnalyzerServer/Http/Adapters/IHttpListenerRequest.cs b/src/RoslynAnalyzerServer/Http/Adapters/IHttpListenerRequest.cs new file mode 100644 index 0000000000..35670dec3b --- /dev/null +++ b/src/RoslynAnalyzerServer/Http/Adapters/IHttpListenerRequest.cs @@ -0,0 +1,48 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Specialized; +using System.IO; +using System.Net; +using System.Text; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Adapters; + +public interface IHttpListenerRequest +{ + IPEndPoint? RemoteEndPoint { get; } + NameValueCollection Headers { get; } + string HttpMethod { get; } + Uri Url { get; } + long ContentLength64 { get; } + Stream InputStream { get; } + Encoding ContentEncoding { get; } +} + +public class HttpListenerRequestAdapter(HttpListenerRequest request) : IHttpListenerRequest +{ + public IPEndPoint? RemoteEndPoint => request.RemoteEndPoint; + public NameValueCollection Headers => request.Headers; + public string HttpMethod => request.HttpMethod; + public Uri Url => request.Url; + public long ContentLength64 => request.ContentLength64; + public Stream InputStream => request.InputStream; + public Encoding ContentEncoding => request.ContentEncoding; +} diff --git a/src/SonarQube.Client.Tests/Models/ServerSentEvents/SSEStreamReaderFactoryTests.cs b/src/RoslynAnalyzerServer/Http/Adapters/IHttpListenerResponse.cs similarity index 56% rename from src/SonarQube.Client.Tests/Models/ServerSentEvents/SSEStreamReaderFactoryTests.cs rename to src/RoslynAnalyzerServer/Http/Adapters/IHttpListenerResponse.cs index cf7a2bcf76..e05f166c6b 100644 --- a/src/SonarQube.Client.Tests/Models/ServerSentEvents/SSEStreamReaderFactoryTests.cs +++ b/src/RoslynAnalyzerServer/Http/Adapters/IHttpListenerResponse.cs @@ -19,27 +19,25 @@ */ using System.IO; -using System.Threading; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using SonarQube.Client.Logging; -using SonarQube.Client.Models.ServerSentEvents; +using System.Net; -namespace SonarQube.Client.Tests.Models.ServerSentEvents +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Adapters; + +public interface IHttpListenerResponse +{ + int StatusCode { get; set; } + long ContentLength64 { get; set; } + Stream OutputStream { get; } + + void Close(); +} + +public class HttpListenerResponseAdapter(HttpListenerResponse response) : IHttpListenerResponse { - [TestClass] - public class SSEStreamReaderFactoryTests - { - [TestMethod] - public void Create_CreatesSSEStreamReader() - { - var testSubject = new SSEStreamReaderFactory(Mock.Of()); + public int StatusCode { get => response.StatusCode; set => response.StatusCode = value; } - var result = testSubject.Create(Stream.Null, CancellationToken.None); + public void Close() => response.Close(); - result.Should().NotBeNull(); - result.Should().BeOfType(); - } - } + public long ContentLength64 { get => response.ContentLength64; set => response.ContentLength64 = value; } + public Stream OutputStream => response.OutputStream; } diff --git a/src/RoslynAnalyzerServer/Http/AnalysisRequestHandler.cs b/src/RoslynAnalyzerServer/Http/AnalysisRequestHandler.cs new file mode 100644 index 0000000000..877f9f1b0e --- /dev/null +++ b/src/RoslynAnalyzerServer/Http/AnalysisRequestHandler.cs @@ -0,0 +1,175 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using System.IO; +using System.Net; +using System.Net.Http; +using Newtonsoft.Json; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Adapters; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http; + +public enum RequestType +{ + Unknown, + Analyze, + Cancel +} + +public interface IAnalysisRequestHandler +{ + Task ParseAnalysisRequestBodyAsync(IHttpListenerRequest request); + + Task ParseCancellationRequestBodyAsync(IHttpListenerRequest request); + + string SerializeAnalysisRequestResponse(List diagnostics); + + bool ValidateRequest(IHttpListenerRequest request, out HttpStatusCode errorCode, out RequestType requestType); +} + +[Export(typeof(IAnalysisRequestHandler))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +internal class AnalysisRequestHandler(ILogger logger, IHttpServerSettings serverSettings, IHttpServerConfigurationProvider serverConfigurationProvider) : IAnalysisRequestHandler +{ + private const string XAuthTokenHeader = "X-Auth-Token"; + private const string AnalyzeRequestUrl = "/analyze"; + private const string CancelAnalysisRequestUrl = "/cancel"; + private readonly ILogger logger = logger.ForContext(Resources.HttpServerLogContext).ForContext(nameof(AnalysisRequestHandler)); + + public string SerializeAnalysisRequestResponse(List diagnostics) + { + var responseObj = new AnalysisResponse { RoslynIssues = diagnostics }; + var responseString = JsonConvert.SerializeObject(responseObj); + return responseString; + } + + public bool ValidateRequest(IHttpListenerRequest request, out HttpStatusCode errorCode, out RequestType requestType) + { + requestType = RequestType.Unknown; + errorCode = default; + return VerifyLocalRequest(request, out errorCode) + && VerifyToken(request, out errorCode) + && VerifyMethod(request, out errorCode, out requestType) + && VerifyContentLength(request, out errorCode); + } + + public async Task ParseAnalysisRequestBodyAsync(IHttpListenerRequest request) + { + var analysisRequestBodyAsync = await ParseAnalysisRequestBodyAsync(request); + return analysisRequestBodyAsync is { FileUris.Count: > 0, ActiveRules.Count: > 0 } ? analysisRequestBodyAsync : null; + } + + public Task ParseCancellationRequestBodyAsync(IHttpListenerRequest request) => + ParseAnalysisRequestBodyAsync(request); + + internal static async Task ParseAnalysisRequestBodyAsync(IHttpListenerRequest request) where T : class + { + var body = await ReadBodyAsync(request); + return GetAnalysisRequestFromBody(body); + } + + private static async Task ReadBodyAsync(IHttpListenerRequest request) + { + using var reader = new StreamReader(request.InputStream, request.ContentEncoding); + return await reader.ReadToEndAsync(); + } + + private static T? GetAnalysisRequestFromBody(string body) where T : class + { + T? requestDto; + try + { + requestDto = JsonConvert.DeserializeObject(body); + } + catch (Exception) + { + return null; + } + return requestDto; + } + + private static bool VerifyLocalRequest(IHttpListenerRequest request, out HttpStatusCode errorCode) + { + if (!IsLocalRequest(request)) + { + errorCode = HttpStatusCode.Forbidden; + return false; + } + errorCode = default; + return true; + } + + private bool VerifyToken(IHttpListenerRequest request, out HttpStatusCode errorCode) + { + var token = request.Headers[XAuthTokenHeader]; + if (token != serverConfigurationProvider.CurrentConfiguration.Token.ToUnsecureString()) + { + errorCode = HttpStatusCode.Unauthorized; + return false; + } + errorCode = default; + return true; + } + + private static bool VerifyMethod(IHttpListenerRequest request, out HttpStatusCode errorCode, out RequestType requestType) + { + errorCode = default; + if (request.HttpMethod == HttpMethod.Post.Method && request.Url.AbsolutePath == AnalyzeRequestUrl) + { + requestType = RequestType.Analyze; + return true; + } + + if (request.HttpMethod == HttpMethod.Post.Method && request.Url.AbsolutePath == CancelAnalysisRequestUrl) + { + requestType = RequestType.Cancel; + return true; + } + + requestType = RequestType.Unknown; + errorCode = HttpStatusCode.BadRequest; + return false; + } + + private bool VerifyContentLength(IHttpListenerRequest request, out HttpStatusCode errorCode) + { + if (request.ContentLength64 > serverSettings.MaxRequestBodyBytes) + { + logger.LogVerbose(Resources.BodyLengthExceeded, request.ContentLength64, serverSettings.MaxRequestBodyBytes); + errorCode = HttpStatusCode.RequestEntityTooLarge; + return false; + } + errorCode = default; + return true; + } + + private static bool IsLocalRequest(IHttpListenerRequest request) + { + var remoteAddress = request.RemoteEndPoint?.Address; + return remoteAddress != null + && (remoteAddress.Equals(IPAddress.Loopback) + || remoteAddress.Equals(IPAddress.IPv6Loopback)); + } +} diff --git a/src/ConnectedMode/ServerSentEvents/QualityProfile/QualityProfileServerEventChannel.cs b/src/RoslynAnalyzerServer/Http/HttpListenerFactory.cs similarity index 60% rename from src/ConnectedMode/ServerSentEvents/QualityProfile/QualityProfileServerEventChannel.cs rename to src/RoslynAnalyzerServer/Http/HttpListenerFactory.cs index f768e26714..e17ef8d3f7 100644 --- a/src/ConnectedMode/ServerSentEvents/QualityProfile/QualityProfileServerEventChannel.cs +++ b/src/RoslynAnalyzerServer/Http/HttpListenerFactory.cs @@ -19,13 +19,26 @@ */ using System.ComponentModel.Composition; -using SonarLint.VisualStudio.Core.ServerSentEvents; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; +using System.Net; -namespace SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.QualityProfile +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http; + +internal interface IHttpListenerFactory { - [Export(typeof(IQualityProfileServerEventSource))] - [Export(typeof(IQualityProfileServerEventSourcePublisher))] - [PartCreationPolicy(CreationPolicy.Shared)] - public class QualityProfileServerEventChannel : ServerEventChannel, IQualityProfileServerEventSource, IQualityProfileServerEventSourcePublisher { } + HttpListener Create(int port); +} + +[Export(typeof(IHttpListenerFactory))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +internal class HttpListenerFactory() : IHttpListenerFactory +{ + public HttpListener Create(int port) + { + var prefix = new UriBuilder(Uri.UriSchemeHttp, IPAddress.Loopback.ToString(), port).Uri.ToString(); + var listener = new HttpListener(); + listener.Prefixes.Add(prefix); + + return listener; + } } diff --git a/src/RoslynAnalyzerServer/Http/HttpRequestHandler.cs b/src/RoslynAnalyzerServer/Http/HttpRequestHandler.cs new file mode 100644 index 0000000000..bd363c08ff --- /dev/null +++ b/src/RoslynAnalyzerServer/Http/HttpRequestHandler.cs @@ -0,0 +1,66 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using System.Net; +using System.Text; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Adapters; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http; + +public interface IHttpRequestHandler +{ + Task SendResponseAsync(IHttpListenerContext context, string responseString); + + void CloseRequest(IHttpListenerContext context, HttpStatusCode statusCode); +} + +[Export(typeof(IHttpRequestHandler))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +internal class HttpRequestHandler() : IHttpRequestHandler +{ + public void CloseRequest(IHttpListenerContext context, HttpStatusCode statusCode) + { + if (!context.Response.OutputStream.CanWrite) + { + return; + } + + context.Response.StatusCode = (int)statusCode; + context.Response.Close(); + } + + public async Task SendResponseAsync(IHttpListenerContext context, string responseString) => await WriteResponseAsync(responseString, context, HttpStatusCode.OK); + + private static async Task WriteResponseAsync(string responseString, IHttpListenerContext context, HttpStatusCode statusCode) + { + if (!context.Response.OutputStream.CanWrite) + { + return; + } + + var buffer = Encoding.UTF8.GetBytes(responseString); + context.Response.ContentLength64 = buffer.Length; + context.Response.StatusCode = (int)statusCode; + await context.Response.OutputStream.WriteAsync(buffer, 0, buffer.Length); + context.Response.OutputStream.Close(); + } +} diff --git a/src/RoslynAnalyzerServer/Http/HttpServerConfigurationProvider.cs b/src/RoslynAnalyzerServer/Http/HttpServerConfigurationProvider.cs new file mode 100644 index 0000000000..fe47241d16 --- /dev/null +++ b/src/RoslynAnalyzerServer/Http/HttpServerConfigurationProvider.cs @@ -0,0 +1,100 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using System.Net; +using System.Net.Sockets; +using System.Security; +using System.Security.Cryptography; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http; + +public interface IHttpServerConfigurationProvider +{ + IHttpServerConfiguration CurrentConfiguration { get; } +} + +internal interface IHttpServerConfigurationFactory +{ + IHttpServerConfiguration SetNewConfiguration(); +} + +[Export(typeof(IHttpServerConfigurationProvider))] +[Export(typeof(IHttpServerConfigurationFactory))] +[PartCreationPolicy(CreationPolicy.Shared)] +internal class HttpServerConfigurationProvider : IHttpServerConfigurationProvider, IHttpServerConfigurationFactory +{ + private readonly object lockObj = new(); + private IHttpServerConfiguration currentConfiguration = null!; + + [ImportingConstructor] + public HttpServerConfigurationProvider() => SetNewConfiguration(); + + public IHttpServerConfiguration SetNewConfiguration() + { + lock (lockObj) + { + currentConfiguration = new HttpServerConfiguration(); + return currentConfiguration; + } + } + + public IHttpServerConfiguration CurrentConfiguration + { + get + { + lock (lockObj) + { + return currentConfiguration; + } + } + } + + private sealed class HttpServerConfiguration : IHttpServerConfiguration + { + private const int TokenByteLength = 32; + private const string PortAnalysisPropertyKey = "sonar.sqvsRoslynPlugin.internal.serverPort"; + private const string TokenAnalysisPropertyKey = "sonar.sqvsRoslynPlugin.internal.serverToken"; + + public int Port { get; } = GetAvailablePort(); + public SecureString Token { get; } = GenerateSecureToken(); + + public Dictionary MapToInferredProperties() => new() { { PortAnalysisPropertyKey, Port.ToString() }, { TokenAnalysisPropertyKey, Token.ToUnsecureString() } }; + + private static int GetAvailablePort() + { + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + listener.Stop(); + return port; + } + + private static SecureString GenerateSecureToken() + { + var bytes = new byte[TokenByteLength]; + using (var rng = new RNGCryptoServiceProvider()) + { + rng.GetBytes(bytes); + } + return Convert.ToBase64String(bytes).ToSecureString(); + } + } +} diff --git a/src/ConnectedMode/ServerSentEvents/Issue/IssueServerEventChannel.cs b/src/RoslynAnalyzerServer/Http/HttpServerSettings.cs similarity index 56% rename from src/ConnectedMode/ServerSentEvents/Issue/IssueServerEventChannel.cs rename to src/RoslynAnalyzerServer/Http/HttpServerSettings.cs index 7da371c2be..1071d4134e 100644 --- a/src/ConnectedMode/ServerSentEvents/Issue/IssueServerEventChannel.cs +++ b/src/RoslynAnalyzerServer/Http/HttpServerSettings.cs @@ -19,13 +19,27 @@ */ using System.ComponentModel.Composition; -using SonarLint.VisualStudio.Core.ServerSentEvents; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; -namespace SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Issue +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http; + +internal interface IHttpServerSettings { - [Export(typeof(IIssueServerEventSource))] - [Export(typeof(IIssueServerEventSourcePublisher))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal class IssueServerEventChannel : ServerEventChannel, IIssueServerEventSource, IIssueServerEventSourcePublisher { } + int MaxStartAttempts { get; } + int RequestMillisecondsTimeout { get; } + long MaxRequestBodyBytes { get; } + int MaxConcurrentRequests { get; } +} + +[Export(typeof(IHttpServerSettings))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +internal class HttpServerSettings() : IHttpServerSettings +{ + private const long OneMb = 1024 * 1024; + private const int ThirtySeconds = 30000; + + public int MaxStartAttempts => 10; + public int RequestMillisecondsTimeout => ThirtySeconds; + public long MaxRequestBodyBytes => OneMb; + public int MaxConcurrentRequests => 20; } diff --git a/src/RoslynAnalyzerServer/Http/IHttpServerConfiguration.cs b/src/RoslynAnalyzerServer/Http/IHttpServerConfiguration.cs new file mode 100644 index 0000000000..aa92a8031b --- /dev/null +++ b/src/RoslynAnalyzerServer/Http/IHttpServerConfiguration.cs @@ -0,0 +1,31 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Security; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http; + +public interface IHttpServerConfiguration +{ + int Port { get; } + SecureString Token { get; } + + Dictionary MapToInferredProperties(); +} diff --git a/src/RoslynAnalyzerServer/Http/Models/ActiveRuleDto.cs b/src/RoslynAnalyzerServer/Http/Models/ActiveRuleDto.cs new file mode 100644 index 0000000000..f8197f5d25 --- /dev/null +++ b/src/RoslynAnalyzerServer/Http/Models/ActiveRuleDto.cs @@ -0,0 +1,25 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; + +public record ActiveRuleDto( + string RuleId, + Dictionary Parameters); diff --git a/src/SonarQube.Client/Api/V6_50/GetQualityProfilesRequest.cs b/src/RoslynAnalyzerServer/Http/Models/AnalysisCancellationRequest.cs similarity index 80% rename from src/SonarQube.Client/Api/V6_50/GetQualityProfilesRequest.cs rename to src/RoslynAnalyzerServer/Http/Models/AnalysisCancellationRequest.cs index 787bece22e..847288ac40 100644 --- a/src/SonarQube.Client/Api/V6_50/GetQualityProfilesRequest.cs +++ b/src/RoslynAnalyzerServer/Http/Models/AnalysisCancellationRequest.cs @@ -20,11 +20,10 @@ using Newtonsoft.Json; -namespace SonarQube.Client.Api.V6_50 +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; + +public record AnalysisCancellationRequest { - public class GetQualityProfilesRequest : V5_20.GetQualityProfilesRequest - { - [JsonProperty("project")] - public override string ProjectKey { get; set; } - } + [JsonRequired] + public Guid AnalysisId { get; set; } } diff --git a/src/RoslynAnalyzerServer/Http/Models/AnalysisRequest.cs b/src/RoslynAnalyzerServer/Http/Models/AnalysisRequest.cs new file mode 100644 index 0000000000..44b71f4c94 --- /dev/null +++ b/src/RoslynAnalyzerServer/Http/Models/AnalysisRequest.cs @@ -0,0 +1,36 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Newtonsoft.Json; +using SonarLint.VisualStudio.SLCore.Common.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; + +public record AnalysisRequest +{ + [JsonRequired] + public List FileUris { get; init; } = []; + [JsonRequired] + public List ActiveRules { get; init; } = []; + public Dictionary AnalysisProperties { get; init; } = []; + public AnalyzerInfoDto AnalyzerInfo { get; init; } = null!; + [JsonRequired] + public Guid AnalysisId { get; init; } +} diff --git a/src/RoslynAnalyzerServer/Http/Models/AnalysisResponse.cs b/src/RoslynAnalyzerServer/Http/Models/AnalysisResponse.cs new file mode 100644 index 0000000000..2a50cf695a --- /dev/null +++ b/src/RoslynAnalyzerServer/Http/Models/AnalysisResponse.cs @@ -0,0 +1,28 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; + +public record AnalysisResponse +{ + public List RoslynIssues { get; set; } = []; +} diff --git a/src/RoslynAnalyzerServer/Http/Models/AnalyzerInfoDto.cs b/src/RoslynAnalyzerServer/Http/Models/AnalyzerInfoDto.cs new file mode 100644 index 0000000000..2096754313 --- /dev/null +++ b/src/RoslynAnalyzerServer/Http/Models/AnalyzerInfoDto.cs @@ -0,0 +1,23 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; + +public record AnalyzerInfoDto(bool ShouldUseCsharpEnterprise, bool ShouldUseVbEnterprise); diff --git a/src/RoslynAnalyzerServer/Http/RoslynAnalysisHttpServer.cs b/src/RoslynAnalyzerServer/Http/RoslynAnalysisHttpServer.cs new file mode 100644 index 0000000000..3054cedaf3 --- /dev/null +++ b/src/RoslynAnalyzerServer/Http/RoslynAnalysisHttpServer.cs @@ -0,0 +1,194 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using System.Net; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Adapters; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http; + +[Export(typeof(IRoslynAnalysisHttpServer))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +internal sealed class RoslynAnalysisHttpServer( + ILogger logger, + IHttpServerSettings settings, + IAnalysisRequestHandler analysisRequestHandler, + IHttpRequestHandler httpRequestHandler, + IHttpListenerFactory httpListenerFactory, + IHttpServerConfigurationFactory httpServerConfigurationFactory, + IRoslynAnalysisService roslynAnalysisService) : IRoslynAnalysisHttpServer +{ + private readonly CancellationTokenSource cancellationTokenSource = new(); + private readonly ILogger logger = logger.ForContext(Resources.RoslynLogContext, Resources.HttpServerLogContext); + private HttpListener? httpListener; + private bool isDisposed; + + public async Task StartListenAsync() + { + try + { + if (httpListener is { IsListening: true } || isDisposed) + { + logger.LogVerbose(Resources.HttpServerNotStarted); + return; + } + + logger.LogVerbose(Resources.HttpServerStarting); + for (var attempt = 1; attempt <= settings.MaxStartAttempts; attempt++) + { + var currentConfiguration = httpServerConfigurationFactory.SetNewConfiguration(); + httpListener = httpListenerFactory.Create(currentConfiguration.Port); + if (cancellationTokenSource.IsCancellationRequested) + { + return; + } + await StartListenAsync(attempt, currentConfiguration.Port); + } + logger.LogVerbose(Resources.HttpServerFailedToStartAttempts, settings.MaxStartAttempts); + } + catch (Exception ex) + { + logger.LogVerbose(Resources.HttpServerFailure, ex); + } + } + + public void Dispose() + { + if (isDisposed) + { + return; + } + cancellationTokenSource.Cancel(); + cancellationTokenSource.Dispose(); + httpListener?.Close(); + roslynAnalysisService.Dispose(); + isDisposed = true; + logger.LogVerbose(Resources.HttpServerDisposed); + } + + private async Task StartListenAsync(int attempt, int port) + { + try + { + httpListener!.Start(); + logger.LogVerbose(Resources.HttpServerStarted); + await WaitForRequestsAsync(httpListener, cancellationTokenSource.Token); + } + catch (HttpListenerException ex) + { + logger.LogVerbose(Resources.HttpServerAttemptFailed, attempt, port, ex.Message); + } + } + + private async Task WaitForRequestsAsync(HttpListener listener, CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + IHttpListenerContext? context = null; + try + { + var getRequestTask = listener.GetContextAsync(); + var completedTask = await Task.WhenAny(getRequestTask, Task.Delay(-1, cancellationToken)); + if (completedTask != getRequestTask) + { + break; + } + context = new HttpListenerContextAdapter(await getRequestTask); + _ = HandleRequestWithTimeoutAsync(context, cancellationToken); + } + catch (Exception ex) + { + logger.WriteLine(Resources.HttpServerAttemptFailed, ex); + if (context != null) + { + httpRequestHandler.CloseRequest(context, HttpStatusCode.InternalServerError); + } + } + } + } + + private async Task HandleRequestWithTimeoutAsync(IHttpListenerContext context, CancellationToken serverCancellationToken) + { + using var requestCancellationToken = new CancellationTokenSource(settings.RequestMillisecondsTimeout); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(serverCancellationToken, requestCancellationToken.Token); + + var requestContext = new MessageLevelContext() { VerboseContext = [ $"Request {Guid.NewGuid().ToString()}"] }; + + try + { + logger.LogVerbose(requestContext, "Received request {0}", context.Request.Url); + var statusCode = await Task.Run(() => HandleRequestAsync(context, linkedCts.Token), linkedCts.Token); + logger.LogVerbose(requestContext, "Response code {0}", statusCode); + } + catch (OperationCanceledException) + { + logger.LogVerbose(requestContext, Resources.HttpRequestTimedOut, settings.RequestMillisecondsTimeout); + httpRequestHandler.CloseRequest(context, HttpStatusCode.RequestTimeout); + } + catch (Exception exception) + { + logger.LogVerbose(requestContext, Resources.HttpRequestFailed, exception.Message + exception.StackTrace); + httpRequestHandler.CloseRequest(context, HttpStatusCode.InternalServerError); + } + } + + private async Task HandleRequestAsync(IHttpListenerContext context, CancellationToken cancellationToken) + { + if (!analysisRequestHandler.ValidateRequest(context.Request, out var validationStatusCode, out var requestType)) + { + httpRequestHandler.CloseRequest(context, validationStatusCode); + return validationStatusCode; + } + + return requestType switch + { + RequestType.Analyze when await analysisRequestHandler.ParseAnalysisRequestBodyAsync(context.Request) is { } analysisRequest => await HandleAnalyzeAsync(context, cancellationToken, analysisRequest), + RequestType.Cancel when await analysisRequestHandler.ParseCancellationRequestBodyAsync(context.Request) is { } cancellationRequest => HandleCancel(context, cancellationRequest), + _ => HandleBadRequest(context) + }; + } + + private HttpStatusCode HandleBadRequest(IHttpListenerContext context) + { + httpRequestHandler.CloseRequest(context, HttpStatusCode.BadRequest); + return HttpStatusCode.BadRequest; + } + + private HttpStatusCode HandleCancel(IHttpListenerContext context, AnalysisCancellationRequest cancellationRequest) + { + var status = roslynAnalysisService.Cancel(cancellationRequest); + var httpStatusCode = status ? HttpStatusCode.OK : HttpStatusCode.NotFound; + httpRequestHandler.CloseRequest(context, httpStatusCode); + return httpStatusCode; + } + + private async Task HandleAnalyzeAsync( + IHttpListenerContext context, + CancellationToken cancellationToken, + AnalysisRequest analysisRequest) + { + var issues = await roslynAnalysisService.AnalyzeAsync(analysisRequest, cancellationToken); + await httpRequestHandler.SendResponseAsync(context, analysisRequestHandler.SerializeAnalysisRequestResponse(issues.ToList())); + return HttpStatusCode.OK; + } +} diff --git a/src/RoslynAnalyzerServer/Http/SecureStringExtensions.cs b/src/RoslynAnalyzerServer/Http/SecureStringExtensions.cs new file mode 100644 index 0000000000..2d2dfa6281 --- /dev/null +++ b/src/RoslynAnalyzerServer/Http/SecureStringExtensions.cs @@ -0,0 +1,69 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Runtime.InteropServices; +using System.Security; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http; + +internal static class SecureStringExtensions +{ + internal static SecureString ToSecureString(this string str) + { + if (str == null) + { + throw new ArgumentNullException(nameof(str)); + } + + var secure = new SecureString(); + foreach (char c in str) + { + secure.AppendChar(c); + } + secure.MakeReadOnly(); + return secure; + } + + // Copied from http://blogs.msdn.com/b/fpintos/archive/2009/06/12/how-to-properly-convert-securestring-to-string.aspx + /// + /// WARNING: This will create plain text version of the in + /// memory which is not encrypted. This could lead to leaking of sensitive information and other security + /// vulnerabilities - heavy caution is advised. + /// + [SecurityCritical] + public static string ToUnsecureString(this SecureString secureString) + { + if (secureString == null) + { + throw new ArgumentNullException(nameof(secureString)); + } + + IntPtr unmanagedString = IntPtr.Zero; + try + { + unmanagedString = Marshal.SecureStringToGlobalAllocUnicode(secureString); + return Marshal.PtrToStringUni(unmanagedString); + } + finally + { + Marshal.ZeroFreeGlobalAllocUnicode(unmanagedString); + } + } +} diff --git a/src/SonarQube.Client/Models/ServerSentEvents/ClientContract/IServerEvent.cs b/src/RoslynAnalyzerServer/IRoslynAnalysisHttpServer.cs similarity index 85% rename from src/SonarQube.Client/Models/ServerSentEvents/ClientContract/IServerEvent.cs rename to src/RoslynAnalyzerServer/IRoslynAnalysisHttpServer.cs index d43c9744e3..4519d907c5 100644 --- a/src/SonarQube.Client/Models/ServerSentEvents/ClientContract/IServerEvent.cs +++ b/src/RoslynAnalyzerServer/IRoslynAnalysisHttpServer.cs @@ -18,9 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -namespace SonarQube.Client.Models.ServerSentEvents.ClientContract +namespace SonarLint.VisualStudio.RoslynAnalyzerServer; + +public interface IRoslynAnalysisHttpServer : IDisposable { - public interface IServerEvent - { - } + Task StartListenAsync(); } diff --git a/src/RoslynAnalyzerServer/IRoslynAnalysisService.cs b/src/RoslynAnalyzerServer/IRoslynAnalysisService.cs new file mode 100644 index 0000000000..17b1e7c3d3 --- /dev/null +++ b/src/RoslynAnalyzerServer/IRoslynAnalysisService.cs @@ -0,0 +1,30 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer; + +internal interface IRoslynAnalysisService : IDisposable +{ + Task> AnalyzeAsync(AnalysisRequest analysisRequest, CancellationToken cancellationToken); + bool Cancel(AnalysisCancellationRequest analysisCancellationRequest); +} diff --git a/src/RoslynAnalyzerServer/IRoslynQuicFixWriter.cs b/src/RoslynAnalyzerServer/IRoslynQuicFixWriter.cs new file mode 100644 index 0000000000..8737bce03d --- /dev/null +++ b/src/RoslynAnalyzerServer/IRoslynQuicFixWriter.cs @@ -0,0 +1,26 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer; + +public interface IRoslynQuickFixStorageWriter +{ + void Add(Guid id, RoslynQuickFixApplicationImpl impl); +} diff --git a/src/RoslynAnalyzerServer/InternalsVisibleTo.cs b/src/RoslynAnalyzerServer/InternalsVisibleTo.cs new file mode 100644 index 0000000000..88513ed7d1 --- /dev/null +++ b/src/RoslynAnalyzerServer/InternalsVisibleTo.cs @@ -0,0 +1,38 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Runtime.CompilerServices; + +#if SignAssembly +[assembly: InternalsVisibleTo("SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests,PublicKey=002400000480000094000000060200000024000052534131000400000100010081b4345a022cc0f4b42bdc795a5a7a1623c1e58dc2246645d751ad41ba98f2749dc5c4e0da3a9e09febcb2cd5b088a0f041f8ac24b20e736d8ae523061733782f9c4cd75b44f17a63714aced0b29a59cd1ce58d8e10ccdb6012c7098c39871043b7241ac4ab9f6b34f183db716082cd57c1ff648135bece256357ba735e67dc6")] +[assembly: InternalsVisibleTo("SonarLint.VisualStudio.RoslynAnalyzerServer.IntegrationTests,PublicKey=002400000480000094000000060200000024000052534131000400000100010081b4345a022cc0f4b42bdc795a5a7a1623c1e58dc2246645d751ad41ba98f2749dc5c4e0da3a9e09febcb2cd5b088a0f041f8ac24b20e736d8ae523061733782f9c4cd75b44f17a63714aced0b29a59cd1ce58d8e10ccdb6012c7098c39871043b7241ac4ab9f6b34f183db716082cd57c1ff648135bece256357ba735e67dc6")] +[assembly: InternalsVisibleTo("SonarLint.VisualStudio.Integration.Vsix.UnitTests,PublicKey=002400000480000094000000060200000024000052534131000400000100010081b4345a022cc0f4b42bdc795a5a7a1623c1e58dc2246645d751ad41ba98f2749dc5c4e0da3a9e09febcb2cd5b088a0f041f8ac24b20e736d8ae523061733782f9c4cd75b44f17a63714aced0b29a59cd1ce58d8e10ccdb6012c7098c39871043b7241ac4ab9f6b34f183db716082cd57c1ff648135bece256357ba735e67dc6")] + +// Moq -- see https://github.com/Moq/moq4/wiki/Quickstart +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] + +#else +[assembly: InternalsVisibleTo("SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests")] +[assembly: InternalsVisibleTo("SonarLint.VisualStudio.RoslynAnalyzerServer.IntegrationTests")] +[assembly: InternalsVisibleTo("SonarLint.VisualStudio.Integration.Vsix.UnitTests")] + +// Moq +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] +#endif diff --git a/src/RoslynAnalyzerServer/Resources.Designer.cs b/src/RoslynAnalyzerServer/Resources.Designer.cs new file mode 100644 index 0000000000..384c2bb96f --- /dev/null +++ b/src/RoslynAnalyzerServer/Resources.Designer.cs @@ -0,0 +1,341 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SonarLint.VisualStudio.RoslynAnalyzerServer.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to No semantic model found for {0}. + /// + internal static string AnalysisCommand_NoSemanticModel { + get { + return ResourceManager.GetString("AnalysisCommand_NoSemanticModel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "No syntax tree found for {0}". + /// + internal static string AnalysisCommand_NoSyntaxTree { + get { + return ResourceManager.GetString("AnalysisCommand_NoSyntaxTree", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Project {0} does not support compilation. + /// + internal static string AnalysisCommandProvider_NoCompilationForProject { + get { + return ResourceManager.GetString("AnalysisCommandProvider_NoCompilationForProject", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No projects to analyze. + /// + internal static string AnalysisCommandProvider_NoProjects { + get { + return ResourceManager.GetString("AnalysisCommandProvider_NoProjects", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "Duplicate diagnostic discarded ID: {0}, File: {1}, Line: {2}". + /// + internal static string AnalysisEngine_DuplicateDiagnostic { + get { + return ResourceManager.GetString("AnalysisEngine_DuplicateDiagnostic", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Project {0} has changed, update no longer valid. + /// + internal static string ApplyChangesOperation_ProjectChanged { + get { + return ResourceManager.GetString("ApplyChangesOperation_ProjectChanged", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Solution projects have changed, update no longer valid. + /// + internal static string ApplyChangesOperation_SolutionChanged { + get { + return ResourceManager.GetString("ApplyChangesOperation_SolutionChanged", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Content length exceeded. Received {0} bytes, max allowed: {1} bytes.. + /// + internal static string BodyLengthExceeded { + get { + return ResourceManager.GetString("BodyLengthExceeded", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Too many concurrent requests. Max allowed {0}.. + /// + internal static string ConcurrentRequestsExceeded { + get { + return ResourceManager.GetString("ConcurrentRequestsExceeded", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Location {0}. + /// + internal static string DefaultSecondaryLocationTitleTemplate { + get { + return ResourceManager.GetString("DefaultSecondaryLocationTitleTemplate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error handling request: {0}.. + /// + internal static string HttpRequestFailed { + get { + return ResourceManager.GetString("HttpRequestFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The analysis request timed out after {0} ms.. + /// + internal static string HttpRequestTimedOut { + get { + return ResourceManager.GetString("HttpRequestTimedOut", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt {0} to start server on port {1} failed due to {2}.. + /// + internal static string HttpServerAttemptFailed { + get { + return ResourceManager.GetString("HttpServerAttemptFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Server stopped and resources disposed.. + /// + internal static string HttpServerDisposed { + get { + return ResourceManager.GetString("HttpServerDisposed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to start server after {0} attempts.. + /// + internal static string HttpServerFailedToStartAttempts { + get { + return ResourceManager.GetString("HttpServerFailedToStartAttempts", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Internal server error {0}.. + /// + internal static string HttpServerFailure { + get { + return ResourceManager.GetString("HttpServerFailure", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Http Server. + /// + internal static string HttpServerLogContext { + get { + return ResourceManager.GetString("HttpServerLogContext", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Server did not start as it was already started or disposed.. + /// + internal static string HttpServerNotStarted { + get { + return ResourceManager.GetString("HttpServerNotStarted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Server started listening.. + /// + internal static string HttpServerStarted { + get { + return ResourceManager.GetString("HttpServerStarted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Staring http server for roslyn analysis.... + /// + internal static string HttpServerStarting { + get { + return ResourceManager.GetString("HttpServerStarting", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to load class {0} from {1}: {2}. + /// + internal static string RoslynAnalysisAnalyzerClassLoaderFailedToLoad { + get { + return ResourceManager.GetString("RoslynAnalysisAnalyzerClassLoaderFailedToLoad", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Analyzer Exception. + /// + internal static string RoslynAnalysisAnalyzerExceptionLogContext { + get { + return ResourceManager.GetString("RoslynAnalysisAnalyzerExceptionLogContext", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to load analyzer {0}: {1}. + /// + internal static string RoslynAnalysisAnalyzerLoaderFailedToLoad { + get { + return ResourceManager.GetString("RoslynAnalysisAnalyzerLoaderFailedToLoad", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Analyzer Loader. + /// + internal static string RoslynAnalysisAnalyzerLoaderLogContext { + get { + return ResourceManager.GetString("RoslynAnalysisAnalyzerLoaderLogContext", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Configuration. + /// + internal static string RoslynAnalysisConfigurationLogContext { + get { + return ResourceManager.GetString("RoslynAnalysisConfigurationLogContext", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No active rules loaded for language {0}. + /// + internal static string RoslynAnalysisConfigurationNoActiveRules { + get { + return ResourceManager.GetString("RoslynAnalysisConfigurationNoActiveRules", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No analyzers loaded for language {0}. + /// + internal static string RoslynAnalysisConfigurationNoAnalyzers { + get { + return ResourceManager.GetString("RoslynAnalysisConfigurationNoAnalyzers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Engine. + /// + internal static string RoslynAnalysisEngineLogContext { + get { + return ResourceManager.GetString("RoslynAnalysisEngineLogContext", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Analysis. + /// + internal static string RoslynAnalysisLogContext { + get { + return ResourceManager.GetString("RoslynAnalysisLogContext", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Roslyn. + /// + internal static string RoslynLogContext { + get { + return ResourceManager.GetString("RoslynLogContext", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to QuickFix. + /// + internal static string RoslynQuickFixLogContext { + get { + return ResourceManager.GetString("RoslynQuickFixLogContext", resourceCulture); + } + } + } +} diff --git a/src/ConnectedMode/QualityProfiles/QualityProfilesStrings.resx b/src/RoslynAnalyzerServer/Resources.resx similarity index 59% rename from src/ConnectedMode/QualityProfiles/QualityProfilesStrings.resx rename to src/RoslynAnalyzerServer/Resources.resx index 8c49109c56..7e23d8add9 100644 --- a/src/ConnectedMode/QualityProfiles/QualityProfilesStrings.resx +++ b/src/RoslynAnalyzerServer/Resources.resx @@ -117,32 +117,97 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Downloading quality profile: {0} - A progress notification message e.g. "Downloading quality profile: C#" + + Content length exceeded. Received {0} bytes, max allowed: {1} bytes. - - Failed to create the configuration for language {0} - Output window message displayed when the application failed to fetch and create the binding configuration associated with a language. {0} the language name. + + Too many concurrent requests. Max allowed {0}. - - Successfully downloaded quality profile. Name: '{0}', Key: '{1}', Language: '{2}' - Output window message indicating the quality profile file was successfully downloaded. {0} quality profile name, {1} quality profile key, {2} language name. + + Error handling request: {0}. - - {0} - Output window message format. Used whenever the message should be displayed as a sub-message from a step message so the user can clearly identify the grouping. {0} the actual message to display. + + The analysis request timed out after {0} ms. - - All quality profiles are up to date + + Attempt {0} to start server on port {1} failed due to {2}. - - [ConnectedMode/QualityProfiles] + + Http Server - - Number of out of date Quality Profiles: {0} + + Server stopped and resources disposed. - - Updating quality profiles... + + Failed to start server after {0} attempts. + + + Internal server error {0}. + + + Server did not start as it was already started or disposed. + + + Server started listening. + + + Staring http server for roslyn analysis... + + + Solution projects have changed, update no longer valid + + + Project {0} has changed, update no longer valid + + + Location {0} + + + Roslyn + + + Analysis + + + Configuration + + + Engine + + + Analyzer Exception + + + No analyzers loaded for language {0} + + + No active rules loaded for language {0} + + + Analyzer Loader + + + Failed to load analyzer {0}: {1} + + + Failed to load class {0} from {1}: {2} + + + QuickFix + + + No projects to analyze + + + Project {0} does not support compilation + + + No semantic model found for {0} + + + "No syntax tree found for {0}" + + + "Duplicate diagnostic discarded ID: {0}, File: {1}, Line: {2}" \ No newline at end of file diff --git a/src/RoslynAnalyzerServer/RoslynAnalysisService.cs b/src/RoslynAnalyzerServer/RoslynAnalysisService.cs new file mode 100644 index 0000000000..4954cdac64 --- /dev/null +++ b/src/RoslynAnalyzerServer/RoslynAnalysisService.cs @@ -0,0 +1,94 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer; + +[Export(typeof(IRoslynAnalysisService))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +internal sealed class RoslynAnalysisService( + IRoslynWorkspaceWrapper workspaceWrapper, + IRoslynAnalysisEngine analysisEngine, + IRoslynAnalysisConfigurationProvider analysisConfigurationProvider, + IRoslynSolutionAnalysisCommandProvider analysisCommandProvider) : IRoslynAnalysisService +{ + private readonly object locker = new(); + private readonly Dictionary cancellationTokensForAnalysis = new(); + + public async Task> AnalyzeAsync( + AnalysisRequest analysisRequest, + CancellationToken cancellationToken) + { + try + { + return await analysisEngine.AnalyzeAsync( + analysisCommandProvider.GetAnalysisCommandsForCurrentSolution(analysisRequest.FileUris.Select(x => x.LocalPath).ToArray()), + await analysisConfigurationProvider.GetConfigurationAsync(analysisRequest.ActiveRules, analysisRequest.AnalysisProperties, analysisRequest.AnalyzerInfo), + SetUpCancellationTokenForAnalysis(analysisRequest, cancellationToken)); + } + finally + { + CancelAndCleanUpToken(analysisRequest.AnalysisId); + } + } + + public bool Cancel(AnalysisCancellationRequest analysisCancellationRequest) + { + return CancelAndCleanUpToken(analysisCancellationRequest.AnalysisId); + } + + private CancellationToken SetUpCancellationTokenForAnalysis( + AnalysisRequest analysisRequest, + CancellationToken cancellationToken) + { + lock (locker) + { + var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cancellationTokensForAnalysis[analysisRequest.AnalysisId] = cancellationTokenSource; + cancellationToken = cancellationTokenSource.Token; + } + return cancellationToken; + } + + private bool CancelAndCleanUpToken(Guid analysisId) + { + CancellationTokenSource? cancellationTokenSource; + lock (locker) + { + if (!cancellationTokensForAnalysis.TryGetValue(analysisId, out cancellationTokenSource)) + { + return false; + } + cancellationTokensForAnalysis.Remove(analysisId); + } + + cancellationTokenSource.Cancel(); + cancellationTokenSource.Dispose(); + return true; + } + + public void Dispose() => workspaceWrapper.Dispose(); +} diff --git a/src/RoslynAnalyzerServer/RoslynAnalyzerServer.csproj b/src/RoslynAnalyzerServer/RoslynAnalyzerServer.csproj new file mode 100644 index 0000000000..f9317efb0b --- /dev/null +++ b/src/RoslynAnalyzerServer/RoslynAnalyzerServer.csproj @@ -0,0 +1,43 @@ + + + + + + + + SonarLint.VisualStudio.RoslynAnalyzerServer + SonarLint.VisualStudio.RoslynAnalyzerServer + enable + true + + + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + diff --git a/src/RoslynAnalyzerServer/RoslynQuickFixApplicationImpl.cs b/src/RoslynAnalyzerServer/RoslynQuickFixApplicationImpl.cs new file mode 100644 index 0000000000..64467955c4 --- /dev/null +++ b/src/RoslynAnalyzerServer/RoslynQuickFixApplicationImpl.cs @@ -0,0 +1,55 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer; + +public class RoslynQuickFixApplicationImpl +{ + private readonly IRoslynWorkspaceWrapper workspace; + private readonly IRoslynSolutionWrapper originalSolution; + internal readonly IRoslynCodeActionWrapper RoslynCodeAction; + + internal RoslynQuickFixApplicationImpl(IRoslynWorkspaceWrapper workspace, IRoslynSolutionWrapper originalSolution, IRoslynCodeActionWrapper roslynCodeAction) + { + this.workspace = workspace; + this.originalSolution = originalSolution; + RoslynCodeAction = roslynCodeAction; + } + + public string Message => RoslynCodeAction.Title; + + public async Task ApplyAsync(CancellationToken cancellationToken) + { + var codeActionOperations = await RoslynCodeAction.GetOperationsAsync(cancellationToken); + + var applyChangesOperation = codeActionOperations.FirstOrDefault(x => x is Microsoft.CodeAnalysis.CodeActions.ApplyChangesOperation) as Microsoft.CodeAnalysis.CodeActions.ApplyChangesOperation; + + // we're only interested in ApplyChangesOperation and there can only be one at a time: https://github.com/dotnet/roslyn/blob/75e79dace86b274327a1afe479228d82a06051a4/src/Workspaces/Core/Portable/CodeActions/Operations/ApplyChangesOperation.cs#L18 + if (applyChangesOperation == null || codeActionOperations.Length > 1) + { + Debug.Fail($"Unexpected quickfix result: {applyChangesOperation} out of {codeActionOperations.Length}"); + return false; + } + + return await workspace.ApplyOrMergeChangesAsync(originalSolution, applyChangesOperation, cancellationToken); + } +} diff --git a/src/RoslynAnalyzerServer/packages.lock.json b/src/RoslynAnalyzerServer/packages.lock.json new file mode 100644 index 0000000000..30432a6f46 --- /dev/null +++ b/src/RoslynAnalyzerServer/packages.lock.json @@ -0,0 +1,437 @@ +{ + "version": 1, + "dependencies": { + ".NETFramework,Version=v4.7.2": { + "Microsoft.CodeAnalysis.Common": { + "type": "Direct", + "requested": "[3.11.0, )", + "resolved": "3.11.0", + "contentHash": "FDKSkRRXnaEWMa2ONkLMo0ZAt/uiV1XIXyodwKIgP1AMIKA7JJKXx/OwFVsvkkUT4BeobLwokoxFw70fICahNg==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.3.2", + "System.Collections.Immutable": "5.0.0", + "System.Memory": "4.5.4", + "System.Reflection.Metadata": "5.0.0", + "System.Runtime.CompilerServices.Unsafe": "5.0.0", + "System.Text.Encoding.CodePages": "4.5.1", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "Microsoft.CodeAnalysis.Workspaces.Common": { + "type": "Direct", + "requested": "[3.11.0, )", + "resolved": "3.11.0", + "contentHash": "YAbH4LCJfh8DhDGwYzSHqvnF06lKkVwblr8C+GwIYCv0i3Rzqjnbversat+i2n9k8twQ43yxVGTYK5p/mIOj4w==", + "dependencies": { + "Humanizer.Core": "2.2.0", + "Microsoft.Bcl.AsyncInterfaces": "5.0.0", + "Microsoft.CodeAnalysis.Common": "[3.11.0]", + "System.Composition": "1.0.31", + "System.IO.Pipelines": "5.0.1" + } + }, + "Microsoft.VisualStudio.LanguageServices": { + "type": "Direct", + "requested": "[3.11.0, )", + "resolved": "3.11.0", + "contentHash": "igs65alqrom0ogrF/ehqc3g1bmekoZERDLHwIOcZDvruRFwhmTo3rBhOTiKEAUChg62aPWIvWB1mVYAgda1tMQ==", + "dependencies": { + "Microsoft.CSharp": "4.3.0", + "Microsoft.CodeAnalysis.Common": "[3.11.0]", + "Microsoft.CodeAnalysis.EditorFeatures.Text": "[3.11.0]", + "Microsoft.CodeAnalysis.Features": "[3.11.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[3.11.0]", + "Microsoft.VisualStudio.Composition": "16.9.20", + "System.Threading.Tasks.Dataflow": "5.0.0" + } + }, + "BouncyCastle.Cryptography": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==" + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.2.0", + "contentHash": "rsYXB7+iUPP8AHgQ8JP2UZI2xK2KhjcdGr9E6zX3CsZaTLCaw8M35vaAJRo1rfxeaZEVMuXeaquLVCkZ7JcZ5Q==" + }, + "MessagePack": { + "type": "Transitive", + "resolved": "2.1.152", + "contentHash": "PlJ31qf42uGuJfwc61x/Pt4hJi01xh1rrBofj1MJSLzEot/2UAIRdSgxEHN/8qou5CV8OBeDM9HXKPi1Oj8rpQ==", + "dependencies": { + "MessagePack.Annotations": "2.1.152", + "Microsoft.Bcl.AsyncInterfaces": "1.0.0", + "System.Memory": "4.5.3", + "System.Reflection.Emit": "4.6.0", + "System.Reflection.Emit.Lightweight": "4.6.0", + "System.Runtime.CompilerServices.Unsafe": "4.5.2", + "System.Threading.Tasks.Extensions": "4.5.3" + } + }, + "MessagePack.Annotations": { + "type": "Transitive", + "resolved": "2.1.152", + "contentHash": "RONktDA/HA641ds/2bfOqYSVew8o8EJMcQ1P4M1J77QGgbzWiWt3nBHvCAwlx0VfO6K9S8xq4b5OLD2CUnhtCg==" + }, + "MessagePackAnalyzer": { + "type": "Transitive", + "resolved": "2.1.152", + "contentHash": "uJhZlGMkXDaFYsH8V9S6o1EyvsUqB9mpU4DVBXNr0DXZVzZMhuLP1IkLj5xK3EKlaAcvkFkZv3eSvuz360wb3Q==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "W8DPQjkMScOMTtJbPwmPyj9c3zYSFGawDW3jwlBOOsnY+EzZFLgNQ/UMkK35JmkNOVPdCyPr2Tw7Vv9N+KA3ZQ==", + "dependencies": { + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.3.2", + "contentHash": "7xt6zTlIEizUgEsYAIgm37EbdkiMmr6fP6J9pDoKEpiGM4pi32BCPGr/IczmSJI9Zzp0a6HOzpr9OvpMP+2veA==" + }, + "Microsoft.CodeAnalysis.AnalyzerUtilities": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "gyQ70pJ4T7hu/s0+QnEaXtYfeG/JrttGnxHJlrhpxsQjRIUGuRhVwNBtkHHYOrUAZ/l47L98/NiJX6QmTwAyrg==" + }, + "Microsoft.CodeAnalysis.EditorFeatures.Text": { + "type": "Transitive", + "resolved": "3.11.0", + "contentHash": "kzTA3dFcBB4M3ft/T4nSPi4fst4N7Vi37zUnWhhs0SAb/jEqeWvD+g4RkB4/0x2pg6YlaaK7ZXqosUqiizhGNw==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[3.11.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[3.11.0]", + "Microsoft.VisualStudio.CoreUtility": "16.10.230", + "Microsoft.VisualStudio.Text.Data": "16.10.230", + "Microsoft.VisualStudio.Text.Logic": "16.10.230", + "Microsoft.VisualStudio.Threading": "16.10.56" + } + }, + "Microsoft.CodeAnalysis.Features": { + "type": "Transitive", + "resolved": "3.11.0", + "contentHash": "FZi4btbqla9fxBbMBOrlwJIvOxmb80E32fQ69IhU4NNQiLSF3LIyOUSQvnV6xl1MccGET7W+NUPtEexsH8GNjA==", + "dependencies": { + "Microsoft.CodeAnalysis.AnalyzerUtilities": "3.3.0", + "Microsoft.CodeAnalysis.Common": "[3.11.0]", + "Microsoft.CodeAnalysis.Scripting.Common": "[3.11.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[3.11.0]", + "Microsoft.DiaSymReader": "1.3.0", + "Microsoft.VisualStudio.Debugger.Contracts": "16.10.0-beta.21214.1", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "Microsoft.CodeAnalysis.Scripting.Common": { + "type": "Transitive", + "resolved": "3.11.0", + "contentHash": "3R0TwSj+74/iDl9R2MDwsGiyrYjCnIKs5a1e17Fa/jGI/C5/c3v/X7tAhnvdXl7pIUnXtTjyWrkVLTv5EKBPmQ==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[3.11.0]" + } + }, + "Microsoft.CSharp": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "P+MBhIM0YX+JqROuf7i306ZLJEjQYA9uUyRDE+OqwUI5sh41e2ZbPQV3LfAPh+29cmceE1pUffXsGfR4eMY3KA==" + }, + "Microsoft.DiaSymReader": { + "type": "Transitive", + "resolved": "1.3.0", + "contentHash": "/fn1Tfo7j7k/slViPlM8azJuxQmri7FZ8dQ+gTeLbI29leN/1VK0U/BFcRdJNctsRCUgyKJ2q+I0Tjq07Rc1/Q==" + }, + "Microsoft.VisualStudio.Composition": { + "type": "Transitive", + "resolved": "16.9.20", + "contentHash": "7vrGl8+9p+vBpqfnQlQ0I2/2qRk3coYxszn9Mu12Xxxoqc/FfHcxJUyGgdqoc0o1EAwkXzKm7FNuyOHFhQ1McQ==", + "dependencies": { + "Microsoft.VisualStudio.Composition.Analyzers": "16.9.20", + "Microsoft.VisualStudio.Composition.NetFxAttributes": "16.9.20", + "Microsoft.VisualStudio.Validation": "15.5.31", + "System.ComponentModel.Composition": "4.5.0", + "System.Composition": "1.0.31", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Metadata": "1.6.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Threading.Tasks.Dataflow": "4.11.1" + } + }, + "Microsoft.VisualStudio.Composition.Analyzers": { + "type": "Transitive", + "resolved": "16.9.20", + "contentHash": "3SoUdxqmeczGmNwifgzlVDMtSYd3tyB/9qoqiN8dl9UVPI3uQKW2XwUQn86l34P3IabJMsj4EbHKZ8q9fkeDGw==" + }, + "Microsoft.VisualStudio.Composition.NetFxAttributes": { + "type": "Transitive", + "resolved": "16.9.20", + "contentHash": "HlPvzs95ONvqML2WsEyu9WpBSMYTwrKugeyyRXeH0x6qDVfYhkSVbZz9nLXJQstNMY+OY7aLu4xKZOwui9f6ag==", + "dependencies": { + "System.ComponentModel.Composition": "4.5.0" + } + }, + "Microsoft.VisualStudio.CoreUtility": { + "type": "Transitive", + "resolved": "16.10.230", + "contentHash": "segnoghs4hfsD3tpM8t+6gcP1PS4a8iHR2GfeAxQjuNQPNnbFSdB376O0wjAx5/qk8H4ZyZJ7fDfv0iP0ZTL/Q==", + "dependencies": { + "Microsoft.VisualStudio.Threading": "16.10.56", + "System.Collections.Immutable": "5.0.0", + "System.ComponentModel.Composition": "4.5.0" + } + }, + "Microsoft.VisualStudio.Debugger.Contracts": { + "type": "Transitive", + "resolved": "16.10.0-beta.21214.1", + "contentHash": "ybpSfj18yDue4O4YAljlkHrCFECjTAy/llhlDFE2NO5e586zdsjfqwJw5+ydVM2WCYQK+CwlB6YboKaqjnKGjA==", + "dependencies": { + "MessagePack": "2.1.152", + "MessagePackAnalyzer": "2.1.152", + "Microsoft.Bcl.AsyncInterfaces": "1.1.1", + "System.Collections.Immutable": "1.5.0" + } + }, + "Microsoft.VisualStudio.Text.Data": { + "type": "Transitive", + "resolved": "16.10.230", + "contentHash": "6uKWOu5nlP5PyxFj8brV2n2k0OY9Ax1/JVgY6QEdhcDMHrCEhIyAiZO4SsDoMmTSv0rXfgxLTZpl/+nQjPyfEg==", + "dependencies": { + "Microsoft.VisualStudio.CoreUtility": "16.10.230", + "Microsoft.VisualStudio.Threading": "16.10.56" + } + }, + "Microsoft.VisualStudio.Text.Logic": { + "type": "Transitive", + "resolved": "16.10.230", + "contentHash": "+eQJ9gkbZfr/pFBJxjHNND9X4e2vjK7AByacbB2+vNen85dXhpwxvZnKYNlnbubtROwmc8CIPSaj+Kg3xHc9kA==", + "dependencies": { + "Microsoft.VisualStudio.CoreUtility": "16.10.230", + "Microsoft.VisualStudio.Text.Data": "16.10.230", + "System.Collections.Immutable": "5.0.0", + "System.ComponentModel.Composition": "4.5.0" + } + }, + "Microsoft.VisualStudio.Threading": { + "type": "Transitive", + "resolved": "16.10.56", + "contentHash": "RjV4Y+/Qh+nSFVPJJXby56W4Th5Z4fi8dBfJY5v8HsO7vA749OsqGdVVfUiXAySsZmJTh6cE9kM0faB/dgIPFA==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "5.0.0", + "Microsoft.VisualStudio.Threading.Analyzers": "16.10.56", + "Microsoft.VisualStudio.Validation": "16.9.32", + "Microsoft.Win32.Registry": "5.0.0", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "Microsoft.VisualStudio.Threading.Analyzers": { + "type": "Transitive", + "resolved": "16.10.56", + "contentHash": "uTvu9fbmHTtRFY5xUgG45waU+ARad0JxyG/2btOC9Su6RTo3PrGVXKU2vHDtVELqlJsnKImhMaPH3YR0XCVwDg==" + }, + "Microsoft.VisualStudio.Validation": { + "type": "Transitive", + "resolved": "16.9.32", + "contentHash": "4Ozqy5OjtU4k9aKT76or3y8tVq6ju8+ba4YZHIe5wTb28jeCg11zkK6bdI4kj8s+k5OSekyO+FkUyoOkvPkgVg==" + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "dependencies": { + "System.Security.AccessControl": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "FXkLXiK0sVVewcso0imKQoOxjoPAj42R8HtjjbSjVPAzwDfzoyoznWxgA3c38LDbN9SJux1xXoXYAhz98j7r2g==", + "dependencies": { + "System.Memory": "4.5.4" + } + }, + "System.ComponentModel.Composition": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "+iB9FoZnfdqMEGq6np28X6YNSUrse16CakmIhV3h6PxEWt7jYxUN3Txs1D8MZhhf4QmyvK0F/EcIN0f4gGN0dA==" + }, + "System.Composition": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "I+D26qpYdoklyAVUdqwUBrEIckMNjAYnuPJy/h9dsQItpQwVREkDFs4b4tkBza0kT2Yk48Lcfsv2QQ9hWsh9Iw==", + "dependencies": { + "System.Composition.AttributedModel": "1.0.31", + "System.Composition.Convention": "1.0.31", + "System.Composition.Hosting": "1.0.31", + "System.Composition.Runtime": "1.0.31", + "System.Composition.TypedParts": "1.0.31" + } + }, + "System.Composition.AttributedModel": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "NHWhkM3ZkspmA0XJEsKdtTt1ViDYuojgSND3yHhTzwxepiwqZf+BCWuvCbjUt4fe0NxxQhUDGJ5km6sLjo9qnQ==" + }, + "System.Composition.Convention": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "GLjh2Ju71k6C0qxMMtl4efHa68NmWeIUYh4fkUI8xbjQrEBvFmRwMDFcylT8/PR9SQbeeL48IkFxU/+gd0nYEQ==", + "dependencies": { + "System.Composition.AttributedModel": "1.0.31" + } + }, + "System.Composition.Hosting": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "fN1bT4RX4vUqjbgoyuJFVUizAl2mYF5VAb+bVIxIYZSSc0BdnX+yGAxcavxJuDDCQ1K+/mdpgyEFc8e9ikjvrg==", + "dependencies": { + "System.Composition.Runtime": "1.0.31" + } + }, + "System.Composition.Runtime": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "0LEJN+2NVM89CE4SekDrrk5tHV5LeATltkp+9WNYrR+Huiyt0vaCqHbbHtVAjPyeLWIc8dOz/3kthRBj32wGQg==" + }, + "System.Composition.TypedParts": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "0Zae/FtzeFgDBBuILeIbC/T9HMYbW4olAmi8XqqAGosSOWvXfiQLfARZEhiGd0LVXaYgXr0NhxiU1LldRP1fpQ==", + "dependencies": { + "System.Composition.AttributedModel": "1.0.31", + "System.Composition.Hosting": "1.0.31", + "System.Composition.Runtime": "1.0.31" + } + }, + "System.IO.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "1h4krG51ZiW/CGzM8gtqrRW2oeG6WZDfPaj27suexL8PxBVahsUlUKMJrqI4kkh6ggHLSDd7MFeU8orpk6COZg==" + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "5.0.1", + "contentHash": "qEePWsaq9LoEEIqhbGe6D5J8c9IqQOUuTzzV6wn1POlfdLkJliZY3OlB0j0f17uMWlqZYjH7txj+2YbyrIA8Yg==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Memory": "4.5.4", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Numerics.Vectors": "4.5.0", + "System.Runtime.CompilerServices.Unsafe": "4.5.3" + } + }, + "System.Numerics.Vectors": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==" + }, + "System.Reflection.Emit": { + "type": "Transitive", + "resolved": "4.6.0", + "contentHash": "qAo4jyXtC9i71iElngX7P2r+zLaiHzxKwf66sc3X91tL5Ks6fnQ1vxL04o7ZSm3sYfLExySL7GN8aTpNYpU1qw==" + }, + "System.Reflection.Emit.Lightweight": { + "type": "Transitive", + "resolved": "4.6.0", + "contentHash": "j/V5HVvxvBQ7uubYD0PptQW2KGsi1Pc2kZ9yfwLixv3ADdjL/4M78KyC5e+ymW612DY8ZE4PFoZmWpoNmN2mqg==" + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "5NecZgXktdGg34rh1OenY1rFNDCI8xSjFr+Z4OU4cU06AQHUdRnIIEeWENu3Wl4YowbzkymAIMvi3WyK9U53pQ==", + "dependencies": { + "System.Collections.Immutable": "5.0.0" + } + }, + "System.Reflection.TypeExtensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", + "dependencies": { + "System.Reflection": "4.3.0" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "ZD9TMpsmYJLrxbbmdvhwt9YEgG5WntEnZ/d1eH8JBX9LBp+Ju8BSBhUGbZMNVHHomWo2KVImJhTDl2hIgw/6MA==" + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", + "dependencies": { + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.Security.Principal.Windows": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" + }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "4J2JQXbftjPMppIHJ7IC+VXQ9XfEagN92vZZNoG12i+zReYlim5dMoXFC1Zzg7tsnKDM7JPo5bYfFK4Jheq44w==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "4.5.2" + } + }, + "System.Threading.Channels": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==", + "dependencies": { + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "System.Threading.Tasks.Dataflow": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "NBp0zSAMZp4muDje6XmbDfmkqw9+qsDCHp+YMEtnVgHEjQZ3Q7MzFTTp3eHqpExn4BwMrS7JkUVOTcVchig4Sw==" + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "4.5.3" + } + }, + "SonarLint.VisualStudio.Core": { + "type": "Project", + "dependencies": { + "BouncyCastle.Cryptography": "[2.4.0, )", + "Newtonsoft.Json": "[13.0.3, )", + "System.Collections.Immutable": "[5.0.0, )", + "System.IO.Abstractions": "[9.0.4, )", + "System.Threading.Channels": "[7.0.0, )" + } + } + } + } +} \ No newline at end of file diff --git a/src/SLCore.IntegrationTests/FileAnalysisTestsRunner.cs b/src/SLCore.IntegrationTests/FileAnalysisTestsRunner.cs index 5c7f20f691..8d6d42dcae 100644 --- a/src/SLCore.IntegrationTests/FileAnalysisTestsRunner.cs +++ b/src/SLCore.IntegrationTests/FileAnalysisTestsRunner.cs @@ -24,6 +24,7 @@ using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Notifications; using SonarLint.VisualStudio.Infrastructure.VS; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http; using SonarLint.VisualStudio.SLCore.Common.Helpers; using SonarLint.VisualStudio.SLCore.Common.Models; using SonarLint.VisualStudio.SLCore.Listener.Analysis; @@ -82,7 +83,7 @@ private FileAnalysisTestsRunner(string testClassName, Dictionary())); slCoreTestRunner.AddListener(analysisListener); slCoreTestRunner.AddListener(listFilesListener); - slCoreTestRunner.AddListener(new AnalysisConfigurationProviderListener(activeConfigScopeTracker, infrastructureLogger)); + slCoreTestRunner.AddListener(new AnalysisConfigurationProviderListener(activeConfigScopeTracker, MockHttpServerConfigurationProvider(), infrastructureLogger)); slCoreTestRunner.AddListener(getFileExclusionsListener); clientFileDtoFactory = new ClientFileDtoFactory(infrastructureLogger); @@ -280,6 +281,15 @@ public void Dispose() activeConfigScopeTracker?.Dispose(); slCoreTestRunner?.Dispose(); } + + private static IHttpServerConfigurationProvider MockHttpServerConfigurationProvider() + { + var provider = Substitute.For(); + var httpServerConfiguration = Substitute.For(); + httpServerConfiguration.MapToInferredProperties().Returns([]); + provider.CurrentConfiguration.Returns(httpServerConfiguration); + return provider; + } } public interface ITestingFile diff --git a/src/SLCore.IntegrationTests/SLCoreTestRunner.cs b/src/SLCore.IntegrationTests/SLCoreTestRunner.cs index 255990428c..70fa9273e9 100644 --- a/src/SLCore.IntegrationTests/SLCoreTestRunner.cs +++ b/src/SLCore.IntegrationTests/SLCoreTestRunner.cs @@ -110,8 +110,9 @@ public async Task Start(TestLogger testLogger) var connectionProvider = Substitute.For(); connectionProvider.GetServerConnections().Returns(new Dictionary()); - var jarProvider = Substitute.For(); + var jarProvider = Substitute.For(); jarProvider.ListJarFiles().Returns(DependencyLocator.AnalyzerPlugins); + jarProvider.ListDisabledPluginKeysForAnalysis().Returns([Language.CSharp.AdditionalPlugins.Single().Key, Language.VBNET.AdditionalPlugins.Single().Key]); var compatibleNodeLocator = Substitute.For(); compatibleNodeLocator.Get().Returns((string)null); @@ -172,7 +173,6 @@ private static void SetLanguagesConfigurationToDefaults(ISLCoreLanguageProvider { var defaultLanguageProvider = new SLCoreLanguageProvider(LanguageProvider.Instance); languageProvider.LanguagesInStandaloneMode.Returns(defaultLanguageProvider.LanguagesInStandaloneMode); - languageProvider.LanguagesWithDisabledAnalysis.Returns(defaultLanguageProvider.LanguagesWithDisabledAnalysis); } public void Dispose() diff --git a/src/SLCore.IntegrationTests/packages.lock.json b/src/SLCore.IntegrationTests/packages.lock.json index 975bb3b8e6..0da00f606f 100644 --- a/src/SLCore.IntegrationTests/packages.lock.json +++ b/src/SLCore.IntegrationTests/packages.lock.json @@ -116,16 +116,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -1260,7 +1250,7 @@ "SonarLint.VisualStudio.Integration": "[1.0.0, )", "SonarLint.VisualStudio.IssueVisualization": "[1.0.0, )", "SonarLint.VisualStudio.IssueVisualization.Security": "[1.0.0, )", - "SonarLint.VisualStudio.Roslyn.Suppressions": "[1.0.0, )", + "SonarLint.VisualStudio.RoslynAnalyzerServer": "[1.0.0, )", "SonarLint.VisualStudio.SLCore": "[1.0.0, )", "SonarLint.VisualStudio.SLCore.Listeners": "[1.0.0, )", "SonarQube.Client": "[1.0.0, )", @@ -1376,16 +1366,10 @@ "SonarLint.VisualStudio.IssueVisualization": "[1.0.0, )" } }, - "SonarLint.VisualStudio.Roslyn.Suppressions": { + "SonarLint.VisualStudio.RoslynAnalyzerServer": { "type": "Project", "dependencies": { - "Microsoft.VisualStudio.Sdk": "[17.0.31902.203, )", - "Newtonsoft.Json": "[13.0.3, )", - "SonarLint.VisualStudio.ConnectedMode": "[1.0.0, )", - "SonarLint.VisualStudio.Core": "[1.0.0, )", - "SonarLint.VisualStudio.Infrastructure.VS": "[1.0.0, )", - "SonarQube.Client": "[1.0.0, )", - "System.IO.Abstractions": "[9.0.4, )" + "SonarLint.VisualStudio.Core": "[1.0.0, )" } }, "SonarLint.VisualStudio.SLCore": { @@ -1400,14 +1384,13 @@ "dependencies": { "SonarLint.VisualStudio.ConnectedMode": "[1.0.0, )", "SonarLint.VisualStudio.IssueVisualization.Security": "[1.0.0, )", + "SonarLint.VisualStudio.RoslynAnalyzerServer": "[1.0.0, )", "SonarLint.VisualStudio.SLCore": "[1.0.0, )" } }, "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/SLCore.Listeners.UnitTests/BranchListenerTests.cs b/src/SLCore.Listeners.UnitTests/BranchListenerTests.cs index b178509771..5d6a0ce25b 100644 --- a/src/SLCore.Listeners.UnitTests/BranchListenerTests.cs +++ b/src/SLCore.Listeners.UnitTests/BranchListenerTests.cs @@ -19,6 +19,7 @@ */ using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.ConfigurationScope; using SonarLint.VisualStudio.SLCore.Core; using SonarLint.VisualStudio.SLCore.Listener.Branch; @@ -27,54 +28,91 @@ namespace SonarLint.VisualStudio.SLCore.Listeners.UnitTests [TestClass] public class BranchListenerTests { - private IStatefulServerBranchProvider statefulServerBranchProvider; + private IServerBranchProvider serverBranchProvider; + private IActiveConfigScopeTracker activeConfigScopeTracker; + private TestLogger logger; private BranchListener testSubject; [TestInitialize] public void TestInitialize() { - statefulServerBranchProvider = Substitute.For(); - testSubject = new BranchListener(statefulServerBranchProvider); + serverBranchProvider = Substitute.For(); + activeConfigScopeTracker = Substitute.For(); + logger = Substitute.ForPartsOf(); + + testSubject = new BranchListener(serverBranchProvider, activeConfigScopeTracker, logger); } [TestMethod] - public void MefCtor_CheckIsExported() - { + public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport()); - } + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); [TestMethod] - public void Mef_CheckIsSingleton() + public void Mef_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public async Task MatchSonarProjectBranchAsync_ConfigurationScopeIdDoesNotMatch_ReturnsNull() { - MefTestHelpers.CheckIsSingletonMefComponent(); + var configScope = new ConfigurationScope("different-id"); + activeConfigScopeTracker.Current.Returns(configScope); + var parameters = new MatchSonarProjectBranchParams("config-id", "branch1", ["branch1", "branch2"]); + + var result = await testSubject.MatchSonarProjectBranchAsync(parameters); + + result.matchedSonarBranch.Should().BeNull(); + serverBranchProvider.DidNotReceiveWithAnyArgs().GetServerBranchName(default); } [TestMethod] - public async Task MatchSonarProjectBranch_ReturnsCalculatedBranch() + public async Task MatchSonarProjectBranchAsync_ConfigurationScopeIdMatches_ReturnsMatchingBranch() { - const string checkedOutBranch = "branch2"; + const string configId = "config-id"; + var configScope = new ConfigurationScope(configId); + activeConfigScopeTracker.Current.Returns(configScope); + var parameters = new MatchSonarProjectBranchParams(configId, "main", ["branch1", "branch2", "main"]); + const string expectedBranch = "branch1"; + serverBranchProvider.GetServerBranchName(Arg.Any>()).Returns(expectedBranch); + + var result = await testSubject.MatchSonarProjectBranchAsync(parameters); - statefulServerBranchProvider.GetServerBranchNameAsync(CancellationToken.None).Returns(checkedOutBranch); + result.matchedSonarBranch.Should().Be(expectedBranch); + serverBranchProvider.Received(1).GetServerBranchName(Arg.Is>(list => + list.Count == 3 && + list.Any(b => b.Name == "branch1" && !b.IsMain) && + list.Any(b => b.Name == "branch2" && !b.IsMain) && + list.Any(b => b.Name == "main" && b.IsMain))); + } - var param = new MatchSonarProjectBranchParams("scopeId", - "mainBranch", - ["branch1", "branch2", "mainBranch"]); + [TestMethod] + public async Task DidChangeMatchedSonarProjectBranchAsync_UpdatesConfigScope_WhenIdMatches() + { + const string configId = "config-id"; + const string branchName = "new-branch"; + activeConfigScopeTracker.TryUpdateMatchedBranchOnCurrentConfigScope(configId, branchName).Returns(true); + var parameters = new DidChangeMatchedSonarProjectBranchParams(configId, branchName); - var result = await testSubject.MatchSonarProjectBranchAsync(param); + await testSubject.DidChangeMatchedSonarProjectBranchAsync(parameters); - result.matchedSonarBranch.Should().Be(checkedOutBranch); + activeConfigScopeTracker.Received(1).TryUpdateMatchedBranchOnCurrentConfigScope(configId, branchName); } [TestMethod] - [DataRow(null)] - [DataRow(5)] - [DataRow("something")] - public void DidChangeMatchedSonarProjectBranch_ReturnsTaskCompleted(object parameter) + public async Task DidChangeMatchedSonarProjectBranchAsync_LogsError_WhenIdDoesNotMatch() { - var result = testSubject.DidChangeMatchedSonarProjectBranchAsync(parameter); + const string configId = "config-id"; + const string branchName = "new-branch"; + activeConfigScopeTracker.TryUpdateMatchedBranchOnCurrentConfigScope(configId, branchName).Returns(false); + var configScope = new ConfigurationScope("different-id"); + activeConfigScopeTracker.Current.Returns(configScope); + var parameters = new DidChangeMatchedSonarProjectBranchParams(configId, branchName); + + await testSubject.DidChangeMatchedSonarProjectBranchAsync(parameters); - result.Should().Be(Task.CompletedTask); + activeConfigScopeTracker.Received(1).TryUpdateMatchedBranchOnCurrentConfigScope(configId, branchName); + logger.AssertPartialOutputStringExists(string.Format(SLCoreStrings.ConfigurationScopeMismatch, configId, configScope.Id)); } } } diff --git a/src/SLCore.Listeners.UnitTests/Implementation/Analysis/AnalysisConfigurationProviderListenerTests.cs b/src/SLCore.Listeners.UnitTests/Implementation/Analysis/AnalysisConfigurationProviderListenerTests.cs index 46a22bb4db..07861be27d 100644 --- a/src/SLCore.Listeners.UnitTests/Implementation/Analysis/AnalysisConfigurationProviderListenerTests.cs +++ b/src/SLCore.Listeners.UnitTests/Implementation/Analysis/AnalysisConfigurationProviderListenerTests.cs @@ -21,6 +21,7 @@ using NSubstitute.ReturnsExtensions; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.ConfigurationScope; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http; using SonarLint.VisualStudio.SLCore.Common.Models; using SonarLint.VisualStudio.SLCore.Core; using SonarLint.VisualStudio.SLCore.Listener.Analysis; @@ -32,31 +33,33 @@ namespace SonarLint.VisualStudio.SLCore.Listeners.UnitTests.Implementation.Analy public class AnalysisConfigurationProviderListenerTests { private IActiveConfigScopeTracker activeConfigScopeTracker; - private AnalysisConfigurationProviderListener testSubject; + private IHttpServerConfigurationProvider httpServerConfigurationProvider; private TestLogger logger; + private AnalysisConfigurationProviderListener testSubject; [TestInitialize] public void TestInitialize() { activeConfigScopeTracker = Substitute.For(); + httpServerConfigurationProvider = Substitute.For(); logger = Substitute.ForPartsOf(); testSubject = new AnalysisConfigurationProviderListener( - activeConfigScopeTracker, logger); + activeConfigScopeTracker, httpServerConfigurationProvider, logger); } [TestMethod] public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported( MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); [TestMethod] public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); [TestMethod] - public void Ctor_InitializesLogContexts() => - logger.Received(1).ForContext(SLCoreStrings.SLCoreName, SLCoreStrings.SLCoreAnalysisConfigurationLogContext); + public void Ctor_InitializesLogContexts() => logger.Received(1).ForContext(SLCoreStrings.SLCoreName, SLCoreStrings.SLCoreAnalysisConfigurationLogContext); [TestMethod] public void GetBaseDirAsync_NoConfigurationScope_ReturnsNull() @@ -95,15 +98,18 @@ public void GetBaseDirAsync_ConfigurationScopeIdMatches_ReturnsCommandsBaseDir() [DataRow(null, [new string[0]])] [DataRow("", [new string[0]])] - [DataRow("configScopeId", [new[] {@"C:\file1"}])] - [DataRow("configScopeId123", [new[] {@"C:\file1", @"D:\file"}])] + [DataRow("configScopeId", [new[] { @"C:\file1" }])] + [DataRow("configScopeId123", [new[] { @"C:\file1", @"D:\file" }])] [DataTestMethod] - public void GetInferredAnalysisProperties_AnyValue_ReturnsEmptySet(string configScopeId, string[] files) + public void GetInferredAnalysisProperties_AnyValue_ReturnsHttpServerConfiguration(string configScopeId, string[] files) { + var expectedProperties = new Dictionary { { "prop1", "val1" } }; + httpServerConfigurationProvider.CurrentConfiguration.MapToInferredProperties().Returns(expectedProperties); + var result = testSubject.GetInferredAnalysisPropertiesAsync(new GetInferredAnalysisPropertiesParams(configScopeId, files.Select(x => new FileUri(x)).ToList())) .Result; - result.Should().BeEquivalentTo(new GetInferredAnalysisPropertiesResponse([]),config:options => options.ComparingByMembers()); + result.Should().BeEquivalentTo(new GetInferredAnalysisPropertiesResponse(expectedProperties), options => options.ComparingByMembers()); } } diff --git a/src/SLCore.Listeners.UnitTests/Implementation/Analysis/RaisedFindingProcessorTests.cs b/src/SLCore.Listeners.UnitTests/Implementation/Analysis/RaisedFindingProcessorTests.cs index c45a9b4f76..55cef4e5d7 100644 --- a/src/SLCore.Listeners.UnitTests/Implementation/Analysis/RaisedFindingProcessorTests.cs +++ b/src/SLCore.Listeners.UnitTests/Implementation/Analysis/RaisedFindingProcessorTests.cs @@ -21,13 +21,11 @@ using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Analysis; using SonarLint.VisualStudio.SLCore.Common.Models; -using SonarLint.VisualStudio.SLCore.Configuration; using SonarLint.VisualStudio.SLCore.Listener.Analysis; using SonarLint.VisualStudio.SLCore.Listener.Analysis.Models; using SonarLint.VisualStudio.SLCore.Listeners.Implementation.Analysis; using SonarLint.VisualStudio.SLCore.Protocol; using SonarLint.VisualStudio.SLCore.Service.Rules.Models; -using SloopLanguage = SonarLint.VisualStudio.SLCore.Common.Models.Language; namespace SonarLint.VisualStudio.SLCore.Listeners.UnitTests.Implementation.Analysis; @@ -41,7 +39,6 @@ public class RaisedFindingProcessorTests [TestMethod] public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); @@ -60,7 +57,7 @@ public void RaiseFindings_AnalysisIdIsNull_DoesNotIgnore() testSubject.RaiseFinding(raiseFindingParams, publisher); - raiseFindingParamsToAnalysisIssueConverter.Received().GetAnalysisIssues(fileUri, Arg.Is>(x => !x.Any())); + raiseFindingParamsToAnalysisIssueConverter.Received().GetAnalysisIssues(fileUri, Arg.Is>(x => x.Count() == 2)); publisher.Received().Publish(fileUri.LocalPath, Arg.Is>(x => !x.Any())); } @@ -82,57 +79,6 @@ public void RaiseFindings_NoFindings_Ignores() publisher.Received().Publish(fileUri.LocalPath, Arg.Is>(x => !x.Any())); } - [TestMethod] - public void RaiseFindings_NoKnownLanguages_PublishesEmpty() - { - var analysisId = Guid.NewGuid(); - var isIntermediatePublication = false; - var raiseFindingParams = new RaiseFindingParams("CONFIGURATION_ID", twoFindingsByFileUri, isIntermediatePublication, analysisId); - var publisher = CreatePublisher(); - IRaiseFindingToAnalysisIssueConverter raiseFindingToAnalysisIssueConverter = CreateConverter(twoFindingsByFileUri.Single().Key, [], []); - - var analysisStatusNotifierFactory = CreateAnalysisStatusNotifierFactory(out var analysisStatusNotifier, fileUri.LocalPath); - var constantsProvider = CreateConstantsProviderWithLanguages([]); - - var testSubject = CreateTestSubject(raiseFindingToAnalysisIssueConverter: raiseFindingToAnalysisIssueConverter, - analysisStatusNotifierFactory: analysisStatusNotifierFactory, - slCoreConstantsProvider: constantsProvider); - - testSubject.RaiseFinding(raiseFindingParams, publisher); - - raiseFindingToAnalysisIssueConverter.Received().GetAnalysisIssues(fileUri, Arg.Is>(x => !x.Any())); - publisher.Received().Publish(fileUri.LocalPath, Arg.Is>(x => !x.Any())); - analysisStatusNotifier.DidNotReceiveWithAnyArgs().AnalysisFinished(default, default); - analysisStatusNotifier.Received().AnalysisProgressed(analysisId, 0, FindingsType, isIntermediatePublication); - VerifyCorrectConstantsAreUsed(constantsProvider); - } - - [TestMethod] - public void RaiseFindings_NoSupportedLanguages_PublishesEmpty() - { - var analysisId = Guid.NewGuid(); - var isIntermediatePublication = false; - var raiseFindingParams = new RaiseFindingParams("CONFIGURATION_ID", twoFindingsByFileUri, isIntermediatePublication, analysisId); - var publisher = CreatePublisher(); - IRaiseFindingToAnalysisIssueConverter raiseFindingToAnalysisIssueConverter = CreateConverter(twoFindingsByFileUri.Single().Key, [], []); - - var analysisStatusNotifierFactory = CreateAnalysisStatusNotifierFactory(out var analysisStatusNotifier, fileUri.LocalPath); - var constantsProvider = CreateConstantsProviderWithLanguages([SloopLanguage.JAVA]); - - var testSubject = CreateTestSubject( - raiseFindingToAnalysisIssueConverter: raiseFindingToAnalysisIssueConverter, - analysisStatusNotifierFactory: analysisStatusNotifierFactory, - slCoreConstantsProvider: constantsProvider); - - testSubject.RaiseFinding(raiseFindingParams, publisher); - - raiseFindingToAnalysisIssueConverter.Received().GetAnalysisIssues(fileUri, Arg.Is>(x => !x.Any())); - publisher.Received().Publish(fileUri.LocalPath, Arg.Is>(x => !x.Any())); - analysisStatusNotifier.DidNotReceiveWithAnyArgs().AnalysisFinished(default, default); - analysisStatusNotifier.Received().AnalysisProgressed(analysisId, 0, FindingsType, isIntermediatePublication); - VerifyCorrectConstantsAreUsed(constantsProvider); - } - [TestMethod] public void RaiseFindings_HasNoFileUri_FinishesAnalysis() { @@ -157,40 +103,37 @@ public void RaiseFindings_PublishFindings(bool isIntermediate) { var analysisId = Guid.NewGuid(); var analysisIssue1 = CreateAnalysisIssue("csharpsquid:S100"); - var analysisIssue2 = CreateAnalysisIssue("secrets:S1012"); - var filteredIssues = new[] { analysisIssue1, analysisIssue2 }; + var analysisIssue2 = CreateAnalysisIssue("javascript:S101"); + var analysisIssue3 = CreateAnalysisIssue("secrets:S1012"); + var raisedIssues = new[] { analysisIssue1, analysisIssue2, analysisIssue3 }; var raisedFinding1 = CreateTestFinding("csharpsquid:S100"); var raisedFinding2 = CreateTestFinding("javascript:S101"); var raisedFinding3 = CreateTestFinding("secrets:S1012"); - var filteredRaisedFindings = new[] { raisedFinding1, raisedFinding3 }; - var raisedFindings = new List { raisedFinding1, raisedFinding2, raisedFinding3 }; + var raisedFindingDtos = new List { raisedFinding1, raisedFinding2, raisedFinding3 }; - var findingsByFileUri = new Dictionary> { { fileUri, raisedFindings } }; + var findingsByFileUri = new Dictionary> { { fileUri, raisedFindingDtos } }; var raiseFindingParams = new RaiseFindingParams("CONFIGURATION_ID", findingsByFileUri, isIntermediate, analysisId); var publisher = CreatePublisher(); IRaiseFindingToAnalysisIssueConverter raiseFindingToAnalysisIssueConverter = - CreateConverter(findingsByFileUri.Single().Key, filteredRaisedFindings, filteredIssues); + CreateConverter(findingsByFileUri.Single().Key, raisedFindingDtos, raisedIssues); var analysisStatusNotifierFactory = CreateAnalysisStatusNotifierFactory(out var analysisStatusNotifier, fileUri.LocalPath); - var constantsProvider = CreateConstantsProviderWithLanguages(SloopLanguage.SECRETS, SloopLanguage.CS); var testSubject = CreateTestSubject( raiseFindingToAnalysisIssueConverter: raiseFindingToAnalysisIssueConverter, - analysisStatusNotifierFactory: analysisStatusNotifierFactory, - slCoreConstantsProvider: constantsProvider); + analysisStatusNotifierFactory: analysisStatusNotifierFactory); testSubject.RaiseFinding(raiseFindingParams, publisher); - publisher.Received(1).Publish(fileUri.LocalPath, filteredIssues); + publisher.Received(1).Publish(fileUri.LocalPath, raisedIssues); raiseFindingToAnalysisIssueConverter.Received(1).GetAnalysisIssues(findingsByFileUri.Single().Key, Arg.Is>( - x => x.SequenceEqual(filteredRaisedFindings))); + x => x.SequenceEqual(raisedFindingDtos))); analysisStatusNotifierFactory.Received(1).Create([fileUri.LocalPath]); analysisStatusNotifier.DidNotReceiveWithAnyArgs().AnalysisFinished(default, default); - analysisStatusNotifier.Received().AnalysisProgressed(analysisId, 2, FindingsType, isIntermediate); - VerifyCorrectConstantsAreUsed(constantsProvider); + analysisStatusNotifier.Received().AnalysisProgressed(analysisId, 3, FindingsType, isIntermediate); } [DataRow(true)] @@ -241,11 +184,8 @@ public void RaiseFindings_MultipleFiles_PublishFindingsForEachFile(bool isInterm private RaisedFindingProcessor CreateTestSubject( IRaiseFindingToAnalysisIssueConverter raiseFindingToAnalysisIssueConverter = null, IAnalysisStatusNotifierFactory analysisStatusNotifierFactory = null, - ILogger logger = null, - ISLCoreLanguageProvider slCoreConstantsProvider = null) => + ILogger logger = null) => new( - slCoreConstantsProvider ?? - CreateConstantsProviderWithLanguages(SloopLanguage.SECRETS, SloopLanguage.JS, SloopLanguage.TS, SloopLanguage.CSS), raiseFindingToAnalysisIssueConverter ?? Substitute.For(), analysisStatusNotifierFactory ?? Substitute.For(), logger ?? new TestLogger()); @@ -260,13 +200,6 @@ private static IRaiseFindingToAnalysisIssueConverter CreateConverter( return raiseFindingParamsToAnalysisIssueConverter; } - private ISLCoreLanguageProvider CreateConstantsProviderWithLanguages(params SloopLanguage[] languages) - { - var slCoreLanguageProvider = Substitute.For(); - slCoreLanguageProvider.AllAnalyzableLanguages.Returns(languages.ToList()); - return slCoreLanguageProvider; - } - private IAnalysisStatusNotifierFactory CreateAnalysisStatusNotifierFactory( out IAnalysisStatusNotifier analysisStatusNotifier, string filePath) @@ -304,14 +237,6 @@ private static IAnalysisIssue CreateAnalysisIssue(string ruleKey) return analysisIssue1; } - private static void VerifyCorrectConstantsAreUsed(ISLCoreLanguageProvider languageProvider) - { - _ = languageProvider.DidNotReceive().LanguagesInStandaloneMode; - _ = languageProvider.DidNotReceive().ExtraLanguagesInConnectedMode; - _ = languageProvider.DidNotReceive().LanguagesWithDisabledAnalysis; - _ = languageProvider.Received().AllAnalyzableLanguages; - } - private record TestFinding( Guid id, string serverKey, diff --git a/src/SLCore.Listeners.UnitTests/packages.lock.json b/src/SLCore.Listeners.UnitTests/packages.lock.json index cb60c76013..922e1126b4 100644 --- a/src/SLCore.Listeners.UnitTests/packages.lock.json +++ b/src/SLCore.Listeners.UnitTests/packages.lock.json @@ -132,16 +132,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.1.194", @@ -1321,6 +1311,12 @@ "SonarLint.VisualStudio.IssueVisualization": "[1.0.0, )" } }, + "SonarLint.VisualStudio.RoslynAnalyzerServer": { + "type": "Project", + "dependencies": { + "SonarLint.VisualStudio.Core": "[1.0.0, )" + } + }, "SonarLint.VisualStudio.SLCore": { "type": "Project", "dependencies": { @@ -1333,14 +1329,13 @@ "dependencies": { "SonarLint.VisualStudio.ConnectedMode": "[1.0.0, )", "SonarLint.VisualStudio.IssueVisualization.Security": "[1.0.0, )", + "SonarLint.VisualStudio.RoslynAnalyzerServer": "[1.0.0, )", "SonarLint.VisualStudio.SLCore": "[1.0.0, )" } }, "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/SLCore.Listeners/Implementation/Analysis/AnalysisConfigurationProviderListener.cs b/src/SLCore.Listeners/Implementation/Analysis/AnalysisConfigurationProviderListener.cs index 40e125e076..8238d03fb0 100644 --- a/src/SLCore.Listeners/Implementation/Analysis/AnalysisConfigurationProviderListener.cs +++ b/src/SLCore.Listeners/Implementation/Analysis/AnalysisConfigurationProviderListener.cs @@ -21,6 +21,7 @@ using System.ComponentModel.Composition; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.ConfigurationScope; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http; using SonarLint.VisualStudio.SLCore.Core; using SonarLint.VisualStudio.SLCore.Listener.Analysis; @@ -29,7 +30,8 @@ namespace SonarLint.VisualStudio.SLCore.Listeners.Implementation.Analysis; [Export(typeof(ISLCoreListener))] [PartCreationPolicy(CreationPolicy.Shared)] [method: ImportingConstructor] -internal class AnalysisConfigurationProviderListener(IActiveConfigScopeTracker activeConfigScopeTracker, ILogger logger) : IAnalysisConfigurationProviderListener +internal class AnalysisConfigurationProviderListener(IActiveConfigScopeTracker activeConfigScopeTracker, IHttpServerConfigurationProvider httpServerConfigurationProvider, ILogger logger) + : IAnalysisConfigurationProviderListener { private readonly ILogger analysisConfigLogger = logger.ForContext(SLCoreStrings.SLCoreName, SLCoreStrings.SLCoreAnalysisConfigurationLogContext); @@ -52,5 +54,5 @@ public Task GetBaseDirAsync(GetBaseDirParams parameters) } public Task GetInferredAnalysisPropertiesAsync(GetInferredAnalysisPropertiesParams parameters) => - Task.FromResult(new GetInferredAnalysisPropertiesResponse([])); + Task.FromResult(new GetInferredAnalysisPropertiesResponse(httpServerConfigurationProvider.CurrentConfiguration.MapToInferredProperties())); } diff --git a/src/SLCore.Listeners/Implementation/Analysis/RaisedFindingProcessor.cs b/src/SLCore.Listeners/Implementation/Analysis/RaisedFindingProcessor.cs index 8f6c8cefac..8402e8a3af 100644 --- a/src/SLCore.Listeners/Implementation/Analysis/RaisedFindingProcessor.cs +++ b/src/SLCore.Listeners/Implementation/Analysis/RaisedFindingProcessor.cs @@ -21,8 +21,6 @@ using System.ComponentModel.Composition; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Analysis; -using SonarLint.VisualStudio.SLCore.Common.Helpers; -using SonarLint.VisualStudio.SLCore.Configuration; using SonarLint.VisualStudio.SLCore.Listener.Analysis; using SonarLint.VisualStudio.SLCore.Listener.Analysis.Models; @@ -37,14 +35,11 @@ internal interface IRaisedFindingProcessor [PartCreationPolicy(CreationPolicy.Shared)] [method: ImportingConstructor] internal class RaisedFindingProcessor( - ISLCoreLanguageProvider slCoreLanguageProvider, IRaiseFindingToAnalysisIssueConverter raiseFindingToAnalysisIssueConverter, IAnalysisStatusNotifierFactory analysisStatusNotifierFactory, ILogger logger) : IRaisedFindingProcessor { - private readonly List analyzableLanguagesRuleKeyPrefixes = CalculateAnalyzableRulePrefixes(slCoreLanguageProvider); - public void RaiseFinding(RaiseFindingParams parameters, IFindingsPublisher findingsPublisher) where T : RaisedFindingDto { if (!IsValid(parameters)) @@ -74,20 +69,10 @@ private void PublishFindings(RaiseFindingParams parameters, IFindingsPubli var fileUri = fileAndIssues.Key; var localPath = fileUri.LocalPath; var analysisStatusNotifier = analysisStatusNotifierFactory.Create([localPath]); - var supportedRaisedIssues = GetSupportedLanguageFindings(fileAndIssues.Value ?? []); + var raisedIssues = fileAndIssues.Value ?? []; findingsPublisher.Publish(localPath, - raiseFindingToAnalysisIssueConverter.GetAnalysisIssues(fileUri, supportedRaisedIssues)); - analysisStatusNotifier.AnalysisProgressed(parameters.analysisId, supportedRaisedIssues.Length, findingsPublisher.FindingsType, parameters.isIntermediatePublication); + raiseFindingToAnalysisIssueConverter.GetAnalysisIssues(fileUri, raisedIssues)); + analysisStatusNotifier.AnalysisProgressed(parameters.analysisId, raisedIssues.Count, findingsPublisher.FindingsType, parameters.isIntermediatePublication); } } - - private T[] GetSupportedLanguageFindings(IEnumerable findings) where T : RaisedFindingDto => - findings.Where(i => analyzableLanguagesRuleKeyPrefixes.Exists(languageRepo => i.ruleKey.StartsWith(languageRepo))).ToArray(); - - private static List CalculateAnalyzableRulePrefixes(ISLCoreLanguageProvider slCoreConstantsProvider) => - slCoreConstantsProvider.AllAnalyzableLanguages? - .Select(x => x.ConvertToCoreLanguage()) - .Select(x => x.RepoInfo?.Key) - .Where(r => r is not null) - .ToList() ?? []; } diff --git a/src/SLCore.Listeners/Implementation/BranchListener.cs b/src/SLCore.Listeners/Implementation/BranchListener.cs index fc85af3e27..615d63618c 100644 --- a/src/SLCore.Listeners/Implementation/BranchListener.cs +++ b/src/SLCore.Listeners/Implementation/BranchListener.cs @@ -20,6 +20,7 @@ using System.ComponentModel.Composition; using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.ConfigurationScope; using SonarLint.VisualStudio.SLCore.Core; using SonarLint.VisualStudio.SLCore.Listener.Branch; @@ -28,24 +29,38 @@ namespace SonarLint.VisualStudio.SLCore.Listeners.Implementation [Export(typeof(ISLCoreListener))] [PartCreationPolicy(CreationPolicy.Shared)] [method: ImportingConstructor] - public class BranchListener(IStatefulServerBranchProvider statefulServerBranchProvider) : IBranchListener + public class BranchListener(IServerBranchProvider serverBranchProvider, IActiveConfigScopeTracker activeConfigScopeTracker, ILogger log) : IBranchListener { + private readonly ILogger log = log.ForContext(SLCoreStrings.SLCoreAnalysisConfigurationLogContext, SLCoreStrings.ConnectedMode_LogContext).ForVerboseContext(nameof(BranchListener)); + /// /// Request to calculate the matching branch between the local project and the sonar server /// - public async Task MatchSonarProjectBranchAsync(MatchSonarProjectBranchParams parameters) + public Task MatchSonarProjectBranchAsync(MatchSonarProjectBranchParams parameters) { - var matchingBranchName = await statefulServerBranchProvider.GetServerBranchNameAsync(CancellationToken.None); - return new MatchSonarProjectBranchResponse(matchingBranchName); + if (activeConfigScopeTracker.Current.Id is var currentId && currentId != parameters.configurationScopeId) + { + log.WriteLine(SLCoreStrings.ConfigurationScopeMismatch, parameters.configurationScopeId, currentId); + return Task.FromResult(new MatchSonarProjectBranchResponse(null)); + } + + var matchingBranchName = serverBranchProvider.GetServerBranchName(parameters.allSonarBranchesNames + .Select(x => new RemoteBranch(x, x == parameters.mainSonarBranchName)) + .ToList()); + + return Task.FromResult(new MatchSonarProjectBranchResponse(matchingBranchName)); } /// - /// Stub method for compability with SLCore. TODO https://github.com/SonarSource/sonarlint-visualstudio/issues/5401 + /// Handles calculated branch notification from SLCore /// - /// Parameter's here for compability we discard it - /// This will be implemented properly in the future when needed but features we support does not need branch awareness for now - public Task DidChangeMatchedSonarProjectBranchAsync(object parameters) + public Task DidChangeMatchedSonarProjectBranchAsync(DidChangeMatchedSonarProjectBranchParams parameters) { + if (!activeConfigScopeTracker.TryUpdateMatchedBranchOnCurrentConfigScope(parameters.configScopeId, parameters.newMatchedBranchName)) + { + log.WriteLine(SLCoreStrings.ConfigurationScopeMismatch, parameters.configScopeId, activeConfigScopeTracker.Current.Id); + } + return Task.CompletedTask; } } diff --git a/src/SLCore.Listeners/SLCore.Listeners.csproj b/src/SLCore.Listeners/SLCore.Listeners.csproj index adcdc92ee2..ff9e540b34 100644 --- a/src/SLCore.Listeners/SLCore.Listeners.csproj +++ b/src/SLCore.Listeners/SLCore.Listeners.csproj @@ -10,6 +10,7 @@ + diff --git a/src/SLCore.Listeners/packages.lock.json b/src/SLCore.Listeners/packages.lock.json index 8d168e41ae..deb2b2e456 100644 --- a/src/SLCore.Listeners/packages.lock.json +++ b/src/SLCore.Listeners/packages.lock.json @@ -47,16 +47,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -1197,6 +1187,12 @@ "SonarLint.VisualStudio.IssueVisualization": "[1.0.0, )" } }, + "SonarLint.VisualStudio.RoslynAnalyzerServer": { + "type": "Project", + "dependencies": { + "SonarLint.VisualStudio.Core": "[1.0.0, )" + } + }, "SonarLint.VisualStudio.SLCore": { "type": "Project", "dependencies": { @@ -1207,8 +1203,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/SLCore.UnitTests/Configuration/SlCoreLanguageProviderTests.cs b/src/SLCore.UnitTests/Configuration/SlCoreLanguageProviderTests.cs index 486d44d2b0..2414878c13 100644 --- a/src/SLCore.UnitTests/Configuration/SlCoreLanguageProviderTests.cs +++ b/src/SLCore.UnitTests/Configuration/SlCoreLanguageProviderTests.cs @@ -57,27 +57,12 @@ public void ExtraLanguagesInConnectedMode_ShouldBeExpected() testSubject.ExtraLanguagesInConnectedMode.Should().BeEquivalentTo(languageProvider.ExtraLanguagesInConnectedMode.Select(x => x.ConvertToSlCoreLanguage())); } - [TestMethod] - public void LanguagesWithDisabledAnalysis_ShouldBeExpected() - { - _ = languageProvider.Received(1).RoslynLanguages; - testSubject.LanguagesWithDisabledAnalysis.Should().BeEquivalentTo(languageProvider.RoslynLanguages.Select(x => x.ConvertToSlCoreLanguage())); - } - - [TestMethod] - public void AllAnalyzableLanguages_ShouldBeExpected() - { - var expected = testSubject.LanguagesInStandaloneMode.Concat(testSubject.ExtraLanguagesInConnectedMode).Except(testSubject.LanguagesWithDisabledAnalysis); - - testSubject.AllAnalyzableLanguages.Should().BeEquivalentTo(expected); - } - private void MockLanguageProvider() { languageProvider = Substitute.For(); // it doesn't have to be the real lists, just need to be different so the test can verify that the provider is using them languageProvider.AllKnownLanguages.Returns([Language.C, Language.Js, Language.Ts, Language.TSql]); - languageProvider.RoslynLanguages.Returns([Language.C]); + languageProvider.RoslynLanguages.Returns([Language.CSharp]); languageProvider.NonRoslynLanguages.Returns([Language.Js]); languageProvider.LanguagesInStandaloneMode.Returns([Language.Ts]); languageProvider.ExtraLanguagesInConnectedMode.Returns([Language.TSql]); diff --git a/src/SLCore.UnitTests/Listener/Analysis/RaiseFindingToAnalysisIssueConverterTests.cs b/src/SLCore.UnitTests/Listener/Analysis/RaiseFindingToAnalysisIssueConverterTests.cs index 52bcc985f6..ecbde8cd1a 100644 --- a/src/SLCore.UnitTests/Listener/Analysis/RaiseFindingToAnalysisIssueConverterTests.cs +++ b/src/SLCore.UnitTests/Listener/Analysis/RaiseFindingToAnalysisIssueConverterTests.cs @@ -24,6 +24,7 @@ using SonarLint.VisualStudio.SLCore.Common.Models; using SonarLint.VisualStudio.SLCore.Listener.Analysis; using SonarLint.VisualStudio.SLCore.Listener.Analysis.Models; +using SonarLint.VisualStudio.SLCore.Protocol; using SonarLint.VisualStudio.SLCore.Service.Rules.Models; using CleanCodeAttribute = SonarLint.VisualStudio.SLCore.Common.Models.CleanCodeAttribute; using SoftwareQuality = SonarLint.VisualStudio.SLCore.Common.Models.SoftwareQuality; @@ -68,31 +69,33 @@ public void GetAnalysisIssues_HasNoIssues_ReturnsEmpty() public void GetAnalysisIssues_HasIssues_ConvertsCorrectly() { var dateTimeOffset = DateTimeOffset.Now; - var issue1 = new RaisedIssueDto( - IssueWithFlowsAndQuickFixesUseCase.Issue1Id, - "serverKey1", - "ruleKey1", - "PrimaryMessage1", - dateTimeOffset, - true, - false, - new TextRangeDto(1, 2, 3, 4), - null, - null, - "context1", - new StandardModeDetails(IssueSeverity.MAJOR, RuleType.CODE_SMELL)); - var issue2 = new RaisedIssueDto(IssueWithFlowsAndQuickFixesUseCase.Issue2Id, - "serverKey2", - "ruleKey2", - "PrimaryMessage2", - dateTimeOffset, - true, - false, - new TextRangeDto(61, 62, 63, 64), - [IssueWithFlowsAndQuickFixesUseCase.Issue2Flow1, IssueWithFlowsAndQuickFixesUseCase.Issue2Flow2], - [IssueWithFlowsAndQuickFixesUseCase.Issue2Fix1, IssueWithFlowsAndQuickFixesUseCase.Issue2Fix2], - "context2", - new MQRModeDetails(CleanCodeAttribute.COMPLETE, IssueWithFlowsAndQuickFixesUseCase.Issue2Impacts)); + var issue1 = CreateRaisedIssueDto( + id: IssueWithFlowsAndQuickFixesUseCase.Issue1Id, + serverKey: "serverKey1", + ruleKey: "ruleKey1", + primaryMessage: "PrimaryMessage1", + introductionDate: dateTimeOffset, + isOnNewCode: true, + resolved: false, + textRange: new TextRangeDto(1, 2, 3, 4), + flows: null, + quickFixes: null, + ruleDescriptionContextKey: "context1", + severityMode: new StandardModeDetails(IssueSeverity.MAJOR, RuleType.CODE_SMELL)); + + var issue2 = CreateRaisedIssueDto( + id: IssueWithFlowsAndQuickFixesUseCase.Issue2Id, + serverKey: "serverKey2", + ruleKey: "ruleKey2", + primaryMessage: "PrimaryMessage2", + introductionDate: dateTimeOffset, + isOnNewCode: true, + resolved: false, + textRange: new TextRangeDto(61, 62, 63, 64), + flows: [IssueWithFlowsAndQuickFixesUseCase.Issue2Flow1, IssueWithFlowsAndQuickFixesUseCase.Issue2Flow2], + quickFixes: [IssueWithFlowsAndQuickFixesUseCase.Issue2Fix1, IssueWithFlowsAndQuickFixesUseCase.Issue2Fix2], + ruleDescriptionContextKey: "context2", + severityMode: new MQRModeDetails(CleanCodeAttribute.COMPLETE, IssueWithFlowsAndQuickFixesUseCase.Issue2Impacts)); var result = testSubject.GetAnalysisIssues(new FileUri("C:\\IssueFile.cs"), new List { issue1, issue2 }).ToList(); @@ -103,33 +106,37 @@ public void GetAnalysisIssues_HasIssues_ConvertsCorrectly() public void GetAnalysisIssues_HasHotspot_ConvertsCorrectly() { var dateTimeOffset = DateTimeOffset.Now; - var issue1 = new RaisedHotspotDto(IssueWithFlowsAndQuickFixesUseCase.Issue1Id, - "serverKey1", - "ruleKey1", - "PrimaryMessage1", - dateTimeOffset, - true, - false, - new TextRangeDto(1, 2, 3, 4), - null, - null, - "context1", - VulnerabilityProbability.HIGH, - HotspotStatus.FIXED, - new StandardModeDetails(IssueSeverity.MAJOR, RuleType.CODE_SMELL)); - var issue2 = new RaisedHotspotDto(IssueWithFlowsAndQuickFixesUseCase.Issue2Id, - "serverKey2", - "ruleKey2", - "PrimaryMessage2", - dateTimeOffset, - true, - false, - new TextRangeDto(61, 62, 63, 64), - [IssueWithFlowsAndQuickFixesUseCase.Issue2Flow1, IssueWithFlowsAndQuickFixesUseCase.Issue2Flow2], - [IssueWithFlowsAndQuickFixesUseCase.Issue2Fix1, IssueWithFlowsAndQuickFixesUseCase.Issue2Fix2], - "context2", VulnerabilityProbability.HIGH, - HotspotStatus.FIXED, - new MQRModeDetails(CleanCodeAttribute.COMPLETE, IssueWithFlowsAndQuickFixesUseCase.Issue2Impacts)); + var issue1 = CreateRaisedHotspotDto( + id: IssueWithFlowsAndQuickFixesUseCase.Issue1Id, + serverKey: "serverKey1", + ruleKey: "ruleKey1", + primaryMessage: "PrimaryMessage1", + introductionDate: dateTimeOffset, + isOnNewCode: true, + resolved: false, + textRange: new TextRangeDto(1, 2, 3, 4), + flows: null, + quickFixes: null, + ruleDescriptionContextKey: "context1", + vulnerabilityProbability: VulnerabilityProbability.HIGH, + status: HotspotStatus.FIXED, + severityMode: new StandardModeDetails(IssueSeverity.MAJOR, RuleType.CODE_SMELL)); + + var issue2 = CreateRaisedHotspotDto( + id: IssueWithFlowsAndQuickFixesUseCase.Issue2Id, + serverKey: "serverKey2", + ruleKey: "ruleKey2", + primaryMessage: "PrimaryMessage2", + introductionDate: dateTimeOffset, + isOnNewCode: true, + resolved: false, + textRange: new TextRangeDto(61, 62, 63, 64), + flows: [IssueWithFlowsAndQuickFixesUseCase.Issue2Flow1, IssueWithFlowsAndQuickFixesUseCase.Issue2Flow2], + quickFixes: [IssueWithFlowsAndQuickFixesUseCase.Issue2Fix1, IssueWithFlowsAndQuickFixesUseCase.Issue2Fix2], + ruleDescriptionContextKey: "context2", + vulnerabilityProbability: VulnerabilityProbability.HIGH, + status: HotspotStatus.FIXED, + severityMode: new MQRModeDetails(CleanCodeAttribute.COMPLETE, IssueWithFlowsAndQuickFixesUseCase.Issue2Impacts)); var result = testSubject.GetAnalysisIssues(new FileUri("C:\\IssueFile.cs"), new List { issue1, issue2 }).ToList(); @@ -141,21 +148,7 @@ public void GetAnalysisIssues_IssueHasUnflattenedFlows_FlattensIntoSingleFlow() { var analysisIssues = testSubject.GetAnalysisIssues(UnflattenedFlowsUseCase.FileUri, new List { - new(Guid.Empty, - default, - default, - default, - default, - default, - default, - new TextRangeDto(1, - 2, - 3, - 4), - UnflattenedFlowsUseCase.UnflattenedFlows, - default, - default, - new StandardModeDetails(default, default)) + CreateRaisedIssueDto(flows: UnflattenedFlowsUseCase.UnflattenedFlows) }); UnflattenedFlowsUseCase.VerifyFlattenedFlow(analysisIssues); @@ -166,23 +159,7 @@ public void GetAnalysisIssues_HotspotHasUnflattenedFlows_FlattensIntoSingleFlow( { var analysisIssues = testSubject.GetAnalysisIssues(UnflattenedFlowsUseCase.FileUri, new List { - new(Guid.Empty, - default, - default, - default, - default, - default, - default, - new TextRangeDto(1, - 2, - 3, - 4), - UnflattenedFlowsUseCase.UnflattenedFlows, - default, - default, - VulnerabilityProbability.HIGH, - HotspotStatus.SAFE, - new StandardModeDetails(default, default)) + CreateRaisedHotspotDto(flows: UnflattenedFlowsUseCase.UnflattenedFlows) }); UnflattenedFlowsUseCase.VerifyFlattenedFlow(analysisIssues); @@ -198,23 +175,8 @@ public void GetAnalysisIssues_HotspotHasVulnerabilityProbability_AnalysisHotspot { var analysisIssues = testSubject.GetAnalysisIssues(fileUri, new List { - new(Guid.Empty, - default, - default, - default, - default, - default, - default, - new TextRangeDto(1, - 2, - 3, - 4), - [], - default, - default, - vulnerabilityProbability, - HotspotStatus.SAFE, - new StandardModeDetails(default, default)) + CreateRaisedHotspotDto( + vulnerabilityProbability: vulnerabilityProbability) }); analysisIssues.Single().Should().BeOfType().Which.HotspotPriority.Should().Be(expectedHotspotPriority); @@ -225,23 +187,7 @@ public void GetAnalysisIssues_HotspotHasNoVulnerabilityProbability_AnalysisHotsp { var analysisIssues = testSubject.GetAnalysisIssues(fileUri, new List { - new(Guid.Empty, - default, - default, - default, - default, - default, - default, - new TextRangeDto(1, - 2, - 3, - 4), - [], - default, - default, - null, - HotspotStatus.SAFE, - new StandardModeDetails(default, default)) + CreateRaisedHotspotDto(vulnerabilityProbability: null) }); analysisIssues.Single().Should().BeOfType().Which.HotspotPriority.Should().BeNull(); @@ -257,23 +203,8 @@ public void GetAnalysisIssues_HotspotWithTwoHighImpactsForDifferentQualities_Get { var analysisIssues = testSubject.GetAnalysisIssues(fileUri, new List { - new(Guid.Empty, - default, - default, - default, - default, - default, - default, - new TextRangeDto(1, - 2, - 3, - 4), - [], - default, - default, - null, - HotspotStatus.SAFE, - new MQRModeDetails(default, + CreateRaisedHotspotDto( + severityMode: new MQRModeDetails(default, [ new ImpactDto(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.INFO), new ImpactDto(SoftwareQuality.RELIABILITY, severity), @@ -298,21 +229,8 @@ public void GetAnalysisIssues_IssueWithTwoHighImpactsForDifferentQualities_GetsT { var analysisIssues = testSubject.GetAnalysisIssues(fileUri, new List { - new(Guid.Empty, - default, - default, - default, - default, - default, - default, - new TextRangeDto(1, - 2, - 3, - 4), - [], - default, - default, - new MQRModeDetails(default, + CreateRaisedIssueDto( + severityMode: new MQRModeDetails(default, [ new ImpactDto(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.INFO), new ImpactDto(SoftwareQuality.RELIABILITY, severity), @@ -333,56 +251,55 @@ public void GetAnalysisIssues_IssueWithTwoHighImpactsForDifferentQualities_GetsT [TestMethod] public void GetAnalysisIssues_TextRangeDtoIsNull_ConvertsCorrectly() { - var dateTimeOffset = DateTimeOffset.Now; - var issue1 = new RaisedHotspotDto(IssueWithFlowsAndQuickFixesUseCase.Issue1Id, - "serverKey1", - "ruleKey1", - "PrimaryMessage1", - dateTimeOffset, - true, - false, - textRange: null, - null, - null, - "context1", - VulnerabilityProbability.HIGH, - HotspotStatus.FIXED, - new StandardModeDetails(IssueSeverity.MAJOR, RuleType.CODE_SMELL)); + const string primaryMessage = "PrimaryMessage1"; + var issue1 = CreateRaisedHotspotDto( + id: IssueWithFlowsAndQuickFixesUseCase.Issue1Id, + primaryMessage: primaryMessage, + textRange: null); var analysisIssues = testSubject.GetAnalysisIssues(new FileUri("C:\\IssueFile.cs"), new List { issue1 }).ToList(); var issue = analysisIssues.SingleOrDefault() as AnalysisIssue; issue.Should().NotBeNull(); issue.PrimaryLocation.TextRange.Should().BeNull(); + issue.PrimaryLocation.Message.Should().Be(primaryMessage); } [TestMethod] public void GetAnalysisIssues_IssueWithQuickFixSplitIntoTwoFileEdits_ReturnsIssueWithSingleQuickFix() { - var issue = new RaisedIssueDto(Guid.NewGuid(), - "serverKey", - "ruleKey", - "PrimaryMessage", - DateTimeOffset.Now, - true, - false, - new TextRangeDto(1, 2, 3, 4), - [], + var issue = CreateRaisedIssueDto( + quickFixes: [ new QuickFixDto([ new FileEditDto(new FileUri("C:\\IssueFile.cs"), [new TextEditDto(new TextRangeDto(5, 6, 7, 8), "new text")]), new FileEditDto(new FileUri("C:\\IssueFile.cs"), [new TextEditDto(new TextRangeDto(9, 10, 11, 12), "another text")]), new FileEditDto(new FileUri("C:\\AnotherFile.cs"), [new TextEditDto(new TextRangeDto(20, 10, 21, 12), "skip this fix")]) - ], "QuickFix")], - "context", - new StandardModeDetails(IssueSeverity.MAJOR, RuleType.CODE_SMELL)); + ], "QuickFix") + ]); var analysisIssues = testSubject.GetAnalysisIssues(new FileUri("C:\\IssueFile.cs"), new List { issue }).ToList(); analysisIssues.Should().NotBeNull(); analysisIssues.Should().ContainSingle(); analysisIssues[0].Fixes.Should().ContainSingle(); - analysisIssues[0].Fixes[0].Edits.Should().HaveCount(2); + analysisIssues[0].Fixes[0].Should().BeAssignableTo().Which.Edits.Should().HaveCount(2); + } + + [TestMethod] + public void GetAnalysisIssues_IssueWithRoslynQuickFix_ReturnsIssueWithRoslynQuickFix() + { + var expectedId = Guid.NewGuid(); + var roslynQuickFix = new RoslynQuickFix(expectedId); + var serializedFix = roslynQuickFix.GetStorageValue(); + + var issue = CreateRaisedIssueDto(quickFixes: [new QuickFixDto([], serializedFix)]); + + var analysisIssues = testSubject.GetAnalysisIssues(new FileUri("C:\\IssueFile.cs"), new List { issue }).ToList(); + + analysisIssues.Should().NotBeNull(); + analysisIssues.Should().ContainSingle(); + analysisIssues[0].Fixes.Should().BeEquivalentTo(roslynQuickFix); } [TestMethod] @@ -473,23 +390,9 @@ public void GetAnalysisIssues_Hotspot_AnalysisHotspotIssueHasHotspotStatus(Hotsp { var analysisIssues = testSubject.GetAnalysisIssues(fileUri, new List { - new(Guid.Empty, - default, - default, - default, - default, - default, - default, - new TextRangeDto(1, - 2, - 3, - 4), - [], - default, - default, - null, - hotspotStatus, - new StandardModeDetails(default, default)) + CreateRaisedHotspotDto( + vulnerabilityProbability: null, + status: hotspotStatus) }); var hotspotIssue = analysisIssues.SingleOrDefault() as AnalysisHotspotIssue; @@ -497,6 +400,68 @@ public void GetAnalysisIssues_Hotspot_AnalysisHotspotIssueHasHotspotStatus(Hotsp hotspotIssue.HotspotStatus.Should().Be(hotspotStatus.ToHotspotStatus()); } + + private static RaisedIssueDto CreateRaisedIssueDto( + Guid? id = null, + string serverKey = null, + string ruleKey = "rule:key", + string primaryMessage = "Primary message", + DateTimeOffset? introductionDate = null, + bool isOnNewCode = true, + bool resolved = false, + TextRangeDto textRange = null, + List flows = null, + List quickFixes = null, + string ruleDescriptionContextKey = null, + Either severityMode = null) + { + return new RaisedIssueDto( + id ?? Guid.NewGuid(), + serverKey, + ruleKey, + primaryMessage, + introductionDate ?? DateTimeOffset.Now, + isOnNewCode, + resolved, + textRange, + flows ?? [], + quickFixes ?? [], + ruleDescriptionContextKey, + severityMode ?? new StandardModeDetails(IssueSeverity.MAJOR, RuleType.CODE_SMELL)); + } + + + private static RaisedHotspotDto CreateRaisedHotspotDto( + Guid? id = null, + string serverKey = null, + string ruleKey = "rule:key", + string primaryMessage = "Primary message", + DateTimeOffset? introductionDate = null, + bool isOnNewCode = true, + bool resolved = false, + TextRangeDto textRange = null, + List flows = null, + List quickFixes = null, + string ruleDescriptionContextKey = null, + VulnerabilityProbability? vulnerabilityProbability = VulnerabilityProbability.HIGH, + HotspotStatus status = HotspotStatus.TO_REVIEW, + Either severityMode = null) => + new( + id ?? Guid.NewGuid(), + serverKey, + ruleKey, + primaryMessage, + introductionDate ?? DateTimeOffset.Now, + isOnNewCode, + resolved, + textRange, + flows ?? [], + quickFixes ?? [], + ruleDescriptionContextKey, + vulnerabilityProbability, + status, + severityMode ?? new StandardModeDetails(IssueSeverity.MAJOR, RuleType.CODE_SMELL)); + private static class UnflattenedFlowsUseCase { internal static FileUri FileUri => new("C:\\IssueFile.cs"); @@ -622,13 +587,14 @@ private static void VerifyIssue2ConvertedCorrectly(List result) result[1].Flows[1].Locations[1].TextRange.LineHash.Should().BeNull(); result[1].Fixes.Should().HaveCount(1); - result[1].Fixes[0].Message.Should().Be("issue 2 fix 2"); - result[1].Fixes[0].Edits.Should().HaveCount(1); - result[1].Fixes[0].Edits[0].RangeToReplace.StartLine.Should().Be(51); - result[1].Fixes[0].Edits[0].RangeToReplace.StartLineOffset.Should().Be(52); - result[1].Fixes[0].Edits[0].RangeToReplace.EndLine.Should().Be(53); - result[1].Fixes[0].Edits[0].RangeToReplace.EndLineOffset.Should().Be(54); - result[1].Fixes[0].Edits[0].RangeToReplace.LineHash.Should().BeNull(); + var quickFix = result[1].Fixes[0].Should().BeAssignableTo().Subject; + quickFix.Message.Should().Be("issue 2 fix 2"); + quickFix.Edits.Should().HaveCount(1); + quickFix.Edits[0].RangeToReplace.StartLine.Should().Be(51); + quickFix.Edits[0].RangeToReplace.StartLineOffset.Should().Be(52); + quickFix.Edits[0].RangeToReplace.EndLine.Should().Be(53); + quickFix.Edits[0].RangeToReplace.EndLineOffset.Should().Be(54); + quickFix.Edits[0].RangeToReplace.LineHash.Should().BeNull(); } } } diff --git a/src/SLCore.UnitTests/SLCoreInstanceFactoryTests.cs b/src/SLCore.UnitTests/SLCoreInstanceFactoryTests.cs index aea11c11dc..b84cf49a5c 100644 --- a/src/SLCore.UnitTests/SLCoreInstanceFactoryTests.cs +++ b/src/SLCore.UnitTests/SLCoreInstanceFactoryTests.cs @@ -43,7 +43,7 @@ public void MefCtor_CheckIsExported() MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), @@ -68,7 +68,7 @@ public void CreateInstance_ReturnsNonNull() var slCoreLanguageProvider = Substitute.For(); var islCoreFoldersProvider = Substitute.For(); var serverConnectionsProvider = Substitute.For(); - var islCoreEmbeddedPluginJarLocator = Substitute.For(); + var slCoreEmbeddedPluginProvider = Substitute.For(); var compatibleNodeLocator = Substitute.For(); var esLintBridgeLocator = Substitute.For(); var activeSolutionBoundTracker = Substitute.For(); @@ -84,7 +84,7 @@ public void CreateInstance_ReturnsNonNull() slCoreLanguageProvider, islCoreFoldersProvider, serverConnectionsProvider, - islCoreEmbeddedPluginJarLocator, + slCoreEmbeddedPluginProvider, compatibleNodeLocator, activeSolutionBoundTracker, configScopeUpdater, diff --git a/src/SLCore.UnitTests/SLCoreInstanceHandleTests.cs b/src/SLCore.UnitTests/SLCoreInstanceHandleTests.cs index f3461e61f6..77b28e69ba 100644 --- a/src/SLCore.UnitTests/SLCoreInstanceHandleTests.cs +++ b/src/SLCore.UnitTests/SLCoreInstanceHandleTests.cs @@ -56,13 +56,14 @@ public class SLCoreInstanceHandleTests private static readonly List JarList = new() { "jar1" }; private static readonly Dictionary ConnectedModeJarList = new() { { "key", "jar1" } }; + private static readonly List DisabledAnalysisPluginKeys = [Language.CS.GetPluginKey()]; private ISLCoreRpcFactory slCoreRpcFactory; private ISLCoreRpcManager rpcManager; private ISLCoreConstantsProvider constantsProvider; private ISLCoreLanguageProvider slCoreLanguageProvider; private ISLCoreFoldersProvider foldersProvider; private IServerConnectionsProvider connectionsProvider; - private ISLCoreEmbeddedPluginJarLocator jarLocator; + private ISLCoreEmbeddedPluginProvider jarProvider; private INodeLocationProvider nodeLocator; private IEsLintBridgeLocator esLintBridgeLocator; private IActiveSolutionBoundTracker activeSolutionBoundTracker; @@ -81,7 +82,7 @@ public void TestInitialize() slCoreLanguageProvider = Substitute.For(); foldersProvider = Substitute.For(); connectionsProvider = Substitute.For(); - jarLocator = Substitute.For(); + jarProvider = Substitute.For(); nodeLocator = Substitute.For(); esLintBridgeLocator = Substitute.For(); activeSolutionBoundTracker = Substitute.For(); @@ -97,7 +98,7 @@ public void TestInitialize() slCoreLanguageProvider, foldersProvider, connectionsProvider, - jarLocator, + jarProvider, nodeLocator, esLintBridgeLocator, activeSolutionBoundTracker, @@ -125,7 +126,7 @@ public void Initialize_RpcManagerThrows_DoesNotCatch() [DataRow(null, null)] public void Initialize_SuccessfullyInitializesInCorrectOrder(string nodeJsPath, string esLintBridgePath) { - SetUpLanguages([], [], []); + SetUpLanguages([], []); SetUpFullConfiguration(out _); nodeLocator.Get().Returns(nodeJsPath); esLintBridgeLocator.Get().Returns(esLintBridgePath); @@ -145,6 +146,7 @@ public void Initialize_SuccessfullyInitializesInCorrectOrder(string nodeJsPath, && parameters.workDir == WorkDir && parameters.embeddedPluginPaths == JarList && parameters.connectedModeEmbeddedPluginPathsByKey.Count == ConnectedModeJarList.Count + && parameters.disabledPluginKeysForAnalysis.SequenceEqual(DisabledAnalysisPluginKeys) && parameters.sonarQubeConnections.SequenceEqual(new[] { SonarQubeConnection1, SonarQubeConnection2 }) && parameters.sonarCloudConnections.SequenceEqual(new[] { SonarCloudConnection }) && parameters.sonarlintUserHome == UserHome @@ -165,8 +167,7 @@ public void Initialize_UsesProvidedLanguageConfiguration() { List standalone = [Language.CS, Language.HTML]; List connected = [Language.VBNET, Language.TSQL]; - List disabledAnalysis = [Language.CPP, Language.JS]; - SetUpLanguages(standalone, connected, disabledAnalysis); + SetUpLanguages(standalone, connected); SetUpFullConfiguration(out _); testSubject.Initialize(); @@ -174,7 +175,6 @@ public void Initialize_UsesProvidedLanguageConfiguration() var initializeParams = (InitializeParams)rpcManager.ReceivedCalls().Single().GetArguments().Single()!; initializeParams.enabledLanguagesInStandaloneMode.Should().BeSameAs(standalone); initializeParams.extraEnabledLanguagesInConnectedMode.Should().BeSameAs(connected); - initializeParams.disabledPluginKeysForAnalysis.Should().BeEquivalentTo(disabledAnalysis.Select(l => l.GetPluginKey())); } [TestMethod] @@ -191,7 +191,7 @@ public void Initialize_ProvidesRulesSettings() [TestMethod] public void Dispose_Initialized_ShutsDownAndDisposesRpc() { - SetUpLanguages([], [], []); + SetUpLanguages([], []); SetUpFullConfiguration(out var rpc); testSubject.Initialize(); @@ -211,7 +211,7 @@ public void Dispose_Initialized_ShutsDownAndDisposesRpc() [TestMethod] public void Dispose_IgnoresShutdownException() { - SetUpLanguages([], [], []); + SetUpLanguages([], []); SetUpFullConfiguration(out var rpc); rpcManager.When(x => x.Shutdown()).Do(_ => throw new Exception()); @@ -226,7 +226,7 @@ public void Dispose_IgnoresShutdownException() [TestMethod] public void Dispose_ConnectionDied_DisposesRpc() { - SetUpLanguages([], [], []); + SetUpLanguages([], []); SetUpFullConfiguration(out var rpc); testSubject.Initialize(); @@ -264,20 +264,19 @@ private void SetUpFullConfiguration(out ISLCoreRpc rpc) { { SonarQubeConnection1.connectionId, SonarQubeConnection1 }, { SonarQubeConnection2.connectionId, SonarQubeConnection2 }, { SonarCloudConnection.connectionId, SonarCloudConnection } }); - jarLocator.ListJarFiles().Returns(JarList); - jarLocator.ListConnectedModeEmbeddedPluginPathsByKey().Returns(ConnectedModeJarList); + jarProvider.ListJarFiles().Returns(JarList); + jarProvider.ListConnectedModeEmbeddedPluginPathsByKey().Returns(ConnectedModeJarList); + jarProvider.ListDisabledPluginKeysForAnalysis().Returns(DisabledAnalysisPluginKeys); activeSolutionBoundTracker.CurrentConfiguration.Returns(new BindingConfiguration(Binding, SonarLintMode.Connected, "dir")); slCoreRuleSettingsProvider.GetSLCoreRuleSettings().Returns(new Dictionary()); } private void SetUpLanguages( List standalone, - List connected, - List disabledAnalysis) + List connected) { slCoreLanguageProvider.LanguagesInStandaloneMode.Returns(standalone); slCoreLanguageProvider.ExtraLanguagesInConnectedMode.Returns(connected); - slCoreLanguageProvider.LanguagesWithDisabledAnalysis.Returns(disabledAnalysis); } #region RpcSetUp diff --git a/src/SLCore.UnitTests/State/ActiveConfigScopeTrackerTests.cs b/src/SLCore.UnitTests/State/ActiveConfigScopeTrackerTests.cs index 0330a21925..d6ac363bf2 100644 --- a/src/SLCore.UnitTests/State/ActiveConfigScopeTrackerTests.cs +++ b/src/SLCore.UnitTests/State/ActiveConfigScopeTrackerTests.cs @@ -163,6 +163,41 @@ public void TryUpdateAnalysisReadinessOnCurrentConfigScope_ConfigScopeDifferent_ VerifyCurrentConfigurationScopeChangedNotRaised(); } + [TestMethod] + public void TryUpdateMatchedBranchOnCurrentConfigScope_ConfigScopeSame_Updates() + { + const string configScopeId = "myid"; + const string connectionId = "connectionid"; + const string sonarProjectKey = "projectkey"; + const string root = "root"; + const string baseDir = "basedir"; + const string branch = "branch"; + testSubject.CurrentConfigScope = new ConfigurationScope(configScopeId, connectionId, sonarProjectKey, root, baseDir, true, branch); + + var result = testSubject.TryUpdateMatchedBranchOnCurrentConfigScope(configScopeId, branch); + + result.Should().BeTrue(); + testSubject.CurrentConfigScope.Should().BeEquivalentTo(new ConfigurationScope(configScopeId, connectionId, sonarProjectKey, root, baseDir, true, branch)); + VerifyCurrentConfigurationScopeChangedRaised(false); + logger.AssertPartialOutputStringExists(string.Format(SLCoreStrings.ConfigScope_UpdatedSonarBranch, configScopeId, branch)); + } + + [TestMethod] + public void TryUpdateMatchedBranchOnCurrentConfigScope_ConfigScopeDifferent_DoesNotUpdate() + { + const string configScopeId = "myid"; + const string connectionId = "connectionid"; + const string sonarProjectKey = "projectkey"; + const string root = "root"; + testSubject.CurrentConfigScope = new ConfigurationScope(configScopeId, connectionId, sonarProjectKey, root); + + var result = testSubject.TryUpdateMatchedBranchOnCurrentConfigScope("some other id", "branch"); + + result.Should().BeFalse(); + testSubject.CurrentConfigScope.Should().BeEquivalentTo(new ConfigurationScope(configScopeId, connectionId, sonarProjectKey, root, MatchedBranch: null)); + VerifyCurrentConfigurationScopeChangedNotRaised(); + } + [TestMethod] public void SetCurrentConfigScope_SetsBoundScope() { diff --git a/src/SLCore.UnitTests/packages.lock.json b/src/SLCore.UnitTests/packages.lock.json index 33a58cfe9b..3e3440eb3a 100644 --- a/src/SLCore.UnitTests/packages.lock.json +++ b/src/SLCore.UnitTests/packages.lock.json @@ -111,16 +111,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -1308,8 +1298,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/SLCore/Configuration/ISLCoreEmbeddedPluginJarLocator.cs b/src/SLCore/Configuration/ISLCoreEmbeddedPluginProvider.cs similarity index 91% rename from src/SLCore/Configuration/ISLCoreEmbeddedPluginJarLocator.cs rename to src/SLCore/Configuration/ISLCoreEmbeddedPluginProvider.cs index ef47f4b4ee..368bf643dd 100644 --- a/src/SLCore/Configuration/ISLCoreEmbeddedPluginJarLocator.cs +++ b/src/SLCore/Configuration/ISLCoreEmbeddedPluginProvider.cs @@ -18,13 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Collections.Generic; - namespace SonarLint.VisualStudio.SLCore.Configuration; -public interface ISLCoreEmbeddedPluginJarLocator +public interface ISLCoreEmbeddedPluginProvider { List ListJarFiles(); Dictionary ListConnectedModeEmbeddedPluginPathsByKey(); + + List ListDisabledPluginKeysForAnalysis(); } diff --git a/src/SLCore/Configuration/ISLCoreLanguageProvider.cs b/src/SLCore/Configuration/ISLCoreLanguageProvider.cs index 4f17e413e6..9f77acf53e 100644 --- a/src/SLCore/Configuration/ISLCoreLanguageProvider.cs +++ b/src/SLCore/Configuration/ISLCoreLanguageProvider.cs @@ -29,8 +29,6 @@ public interface ISLCoreLanguageProvider { IReadOnlyList LanguagesInStandaloneMode { get; } IReadOnlyList ExtraLanguagesInConnectedMode { get; } - IReadOnlyList AllAnalyzableLanguages { get; } - IReadOnlyList LanguagesWithDisabledAnalysis { get; } } [Export(typeof(ISLCoreLanguageProvider))] @@ -42,12 +40,8 @@ public SLCoreLanguageProvider(ILanguageProvider languageProvider) { LanguagesInStandaloneMode = languageProvider.LanguagesInStandaloneMode.Select(x => x.ConvertToSlCoreLanguage()).ToList(); ExtraLanguagesInConnectedMode = languageProvider.ExtraLanguagesInConnectedMode.Select(x => x.ConvertToSlCoreLanguage()).ToList(); - LanguagesWithDisabledAnalysis = languageProvider.RoslynLanguages.Select(x => x.ConvertToSlCoreLanguage()).ToList(); - AllAnalyzableLanguages = LanguagesInStandaloneMode.Concat(ExtraLanguagesInConnectedMode).Except(LanguagesWithDisabledAnalysis).ToList(); } public IReadOnlyList LanguagesInStandaloneMode { get; } public IReadOnlyList ExtraLanguagesInConnectedMode { get; } - public IReadOnlyList LanguagesWithDisabledAnalysis { get; } - public IReadOnlyList AllAnalyzableLanguages { get; } } diff --git a/src/SLCore/ISLCoreInstanceFactory.cs b/src/SLCore/ISLCoreInstanceFactory.cs index 5d5fc3afcc..7e79441a4e 100644 --- a/src/SLCore/ISLCoreInstanceFactory.cs +++ b/src/SLCore/ISLCoreInstanceFactory.cs @@ -46,7 +46,7 @@ internal class SLCoreInstanceFactory : ISLCoreInstanceFactory private readonly ISLCoreLanguageProvider slCoreLanguageProvider; private readonly ISLCoreFoldersProvider slCoreFoldersProvider; private readonly IServerConnectionsProvider serverConnectionConfigurationProvider; - private readonly ISLCoreEmbeddedPluginJarLocator slCoreEmbeddedPluginJarProvider; + private readonly ISLCoreEmbeddedPluginProvider slCoreEmbeddedPluginProvider; private readonly INodeLocationProvider nodeLocator; private readonly IActiveSolutionBoundTracker activeSolutionBoundTracker; private readonly IConfigScopeUpdater configScopeUpdater; @@ -63,7 +63,7 @@ public SLCoreInstanceFactory( ISLCoreLanguageProvider slCoreLanguageProvider, ISLCoreFoldersProvider slCoreFoldersProvider, IServerConnectionsProvider serverConnectionConfigurationProvider, - ISLCoreEmbeddedPluginJarLocator slCoreEmbeddedPluginJarProvider, + ISLCoreEmbeddedPluginProvider slCoreEmbeddedPluginProvider, INodeLocationProvider nodeLocator, IActiveSolutionBoundTracker activeSolutionBoundTracker, IConfigScopeUpdater configScopeUpdater, @@ -78,7 +78,7 @@ public SLCoreInstanceFactory( this.slCoreLanguageProvider = slCoreLanguageProvider; this.slCoreFoldersProvider = slCoreFoldersProvider; this.serverConnectionConfigurationProvider = serverConnectionConfigurationProvider; - this.slCoreEmbeddedPluginJarProvider = slCoreEmbeddedPluginJarProvider; + this.slCoreEmbeddedPluginProvider = slCoreEmbeddedPluginProvider; this.nodeLocator = nodeLocator; this.activeSolutionBoundTracker = activeSolutionBoundTracker; this.configScopeUpdater = configScopeUpdater; @@ -96,7 +96,7 @@ public ISLCoreInstanceHandle CreateInstance() => slCoreLanguageProvider, slCoreFoldersProvider, serverConnectionConfigurationProvider, - slCoreEmbeddedPluginJarProvider, + slCoreEmbeddedPluginProvider, nodeLocator, esLintBridgeLocator, activeSolutionBoundTracker, diff --git a/src/SLCore/ISLCoreInstanceHandle.cs b/src/SLCore/ISLCoreInstanceHandle.cs index fa65d5a87e..b797ad32ed 100644 --- a/src/SLCore/ISLCoreInstanceHandle.cs +++ b/src/SLCore/ISLCoreInstanceHandle.cs @@ -23,7 +23,6 @@ using SonarLint.VisualStudio.Core.Binding; using SonarLint.VisualStudio.Core.ConfigurationScope; using SonarLint.VisualStudio.SLCore.Analysis; -using SonarLint.VisualStudio.SLCore.Common.Helpers; using SonarLint.VisualStudio.SLCore.Configuration; using SonarLint.VisualStudio.SLCore.Core; using SonarLint.VisualStudio.SLCore.EsLintBridge; @@ -52,7 +51,7 @@ internal sealed class SLCoreInstanceHandle : ISLCoreInstanceHandle private readonly ISLCoreConstantsProvider constantsProvider; private readonly ISLCoreLanguageProvider slCoreLanguageProvider; private readonly ISLCoreFoldersProvider slCoreFoldersProvider; - private readonly ISLCoreEmbeddedPluginJarLocator slCoreEmbeddedPluginJarProvider; + private readonly ISLCoreEmbeddedPluginProvider slCoreEmbeddedPluginJarProvider; private readonly ISLCoreRuleSettingsProvider slCoreRuleSettingsProvider; private readonly ISlCoreTelemetryMigrationProvider telemetryMigrationProvider; private readonly IEsLintBridgeLocator esLintBridgeLocator; @@ -68,7 +67,7 @@ internal SLCoreInstanceHandle( ISLCoreLanguageProvider slCoreLanguageProvider, ISLCoreFoldersProvider slCoreFoldersProvider, IServerConnectionsProvider serverConnectionConfigurationProvider, - ISLCoreEmbeddedPluginJarLocator slCoreEmbeddedPluginJarProvider, + ISLCoreEmbeddedPluginProvider slCoreEmbeddedPluginJarProvider, INodeLocationProvider nodeLocator, IEsLintBridgeLocator esLintBridgeLocator, IActiveSolutionBoundTracker activeSolutionBoundTracker, @@ -99,8 +98,6 @@ public void Initialize() SLCoreRpc = slCoreRpcFactory.StartNewRpcInstance(); - - var serverConnectionConfigurations = serverConnectionConfigurationProvider.GetServerConnections(); var (storageRoot, workDir, sonarlintUserHome) = slCoreFoldersProvider.GetWorkFolders(); @@ -114,7 +111,7 @@ public void Initialize() connectedModeEmbeddedPluginPathsByKey: slCoreEmbeddedPluginJarProvider.ListConnectedModeEmbeddedPluginPathsByKey(), enabledLanguagesInStandaloneMode: slCoreLanguageProvider.LanguagesInStandaloneMode, extraEnabledLanguagesInConnectedMode: slCoreLanguageProvider.ExtraLanguagesInConnectedMode, - disabledPluginKeysForAnalysis: slCoreLanguageProvider.LanguagesWithDisabledAnalysis.Select(l => l.GetPluginKey()).ToList(), + disabledPluginKeysForAnalysis: slCoreEmbeddedPluginJarProvider.ListDisabledPluginKeysForAnalysis(), serverConnectionConfigurations.Values.OfType().ToList(), serverConnectionConfigurations.Values.OfType().ToList(), sonarlintUserHome, diff --git a/src/SLCore/Listener/Analysis/RaiseFindingToAnalysisIssueConverter.cs b/src/SLCore/Listener/Analysis/RaiseFindingToAnalysisIssueConverter.cs index 25eb119d67..90484a350f 100644 --- a/src/SLCore/Listener/Analysis/RaiseFindingToAnalysisIssueConverter.cs +++ b/src/SLCore/Listener/Analysis/RaiseFindingToAnalysisIssueConverter.cs @@ -38,9 +38,9 @@ public IEnumerable GetAnalysisIssues(FileUri fileUri, IEnumer raisedFindings .Select(item => TryCreateAnalysisIssue(fileUri, item)) .Where(x => x != null) - .ToList(); + .ToList()!; - private AnalysisIssue TryCreateAnalysisIssue(FileUri fileUri, T item) where T : RaisedFindingDto + private AnalysisIssue? TryCreateAnalysisIssue(FileUri fileUri, T item) where T : RaisedFindingDto { try { @@ -89,7 +89,7 @@ private AnalysisIssue TryCreateAnalysisIssue(FileUri fileUri, T item) where T } } - private static Impact GetHighestImpact(List impacts) + private static Impact? GetHighestImpact(List? impacts) { if (impacts is null || impacts.Count == 0) { @@ -101,7 +101,7 @@ private static Impact GetHighestImpact(List impacts) private static IAnalysisIssueLocation GetAnalysisIssueLocation(string filePath, string message, TextRangeDto textRangeDto) => new AnalysisIssueLocation(message, filePath, CopyTextRange(textRangeDto)); - private static TextRange CopyTextRange(TextRangeDto textRangeDto) + private static TextRange? CopyTextRange(TextRangeDto? textRangeDto) { if (textRangeDto is null) { @@ -114,7 +114,7 @@ private static TextRange CopyTextRange(TextRangeDto textRangeDto) null); } - private static IAnalysisIssueFlow[] GetFlows(List issueFlows) + private static IAnalysisIssueFlow[] GetFlows(List? issueFlows) { if (issueFlows is null || issueFlows.Count == 0) { @@ -132,8 +132,13 @@ private static IAnalysisIssueFlow[] GetFlows(List issueFlows) private static IAnalysisIssueFlow GetAnalysisIssueFlow(IEnumerable flowLocations) => new AnalysisIssueFlow(flowLocations.Select(l => GetAnalysisIssueLocation(l.fileUri.LocalPath, l.message, l.textRange)).ToList()); - private static IQuickFix? GetQuickFix(FileUri fileURi, QuickFixDto quickFixDto) + private static IQuickFixBase? GetQuickFix(FileUri fileURi, QuickFixDto quickFixDto) { + if (RoslynQuickFix.TryParse(quickFixDto.message, out var fix)) + { + return fix; + } + var fileEdits = quickFixDto.inputFileEdits.FindAll(e => e.target == fileURi); if (fileEdits.Count == 0) { @@ -141,7 +146,7 @@ private static IAnalysisIssueFlow GetAnalysisIssueFlow(IEnumerable x.textEdits).Select(GetEdit).ToList(); - return new QuickFix(quickFixDto.message, textEdits); + return new TextBasedQuickFix(quickFixDto.message, textEdits); } private static IEdit GetEdit(TextEditDto textEdit) => diff --git a/src/SLCore/Listener/Branch/DidChangeMatchedSonarProjectBranchParams.cs b/src/SLCore/Listener/Branch/DidChangeMatchedSonarProjectBranchParams.cs new file mode 100644 index 0000000000..23f96d44ed --- /dev/null +++ b/src/SLCore/Listener/Branch/DidChangeMatchedSonarProjectBranchParams.cs @@ -0,0 +1,23 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.SLCore.Listener.Branch; + +public record DidChangeMatchedSonarProjectBranchParams(string configScopeId, string newMatchedBranchName); diff --git a/src/SLCore/Listener/Branch/IBranchListener.cs b/src/SLCore/Listener/Branch/IBranchListener.cs index 837996b28d..7175971a2a 100644 --- a/src/SLCore/Listener/Branch/IBranchListener.cs +++ b/src/SLCore/Listener/Branch/IBranchListener.cs @@ -36,5 +36,5 @@ public interface IBranchListener : ISLCoreListener /// /// Parameter's here for compability we discard it /// This will be implemented properly in the future when needed but features we support does not need branch awareness for now - Task DidChangeMatchedSonarProjectBranchAsync(object parameters); + Task DidChangeMatchedSonarProjectBranchAsync(DidChangeMatchedSonarProjectBranchParams parameters); } diff --git a/src/ConnectedMode/Binding/IBindingConfigProvider.cs b/src/SLCore/Listener/Branch/IServerBranchProvider.cs similarity index 61% rename from src/ConnectedMode/Binding/IBindingConfigProvider.cs rename to src/SLCore/Listener/Branch/IServerBranchProvider.cs index e1740ee5f0..3cfe9751ee 100644 --- a/src/ConnectedMode/Binding/IBindingConfigProvider.cs +++ b/src/SLCore/Listener/Branch/IServerBranchProvider.cs @@ -18,20 +18,21 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarQube.Client.Models; +namespace SonarLint.VisualStudio.SLCore.Listener.Branch; -namespace SonarLint.VisualStudio.ConnectedMode.Binding; +public record struct RemoteBranch(string Name, bool IsMain); -/// -/// Contract to provide the binding-related configuration for one or more languages -/// -public interface IBindingConfigProvider +public interface IServerBranchProvider { /// - /// Returns a configuration file for the specified language + /// Returns the Sonar server branch to use when requesting data /// - Task SaveConfigurationAsync(SonarQubeQualityProfile qualityProfile, Language language, - BindingConfiguration bindingConfiguration, CancellationToken cancellationToken); + /// The Sonar server branch name, + /// or the name of the Sonar server branch marked as "Main" if the branch cannot be determined, + /// or null if we are not in connected mode. + /// + /// + /// Only applies in connected mode. + /// + string? GetServerBranchName(List branches); } diff --git a/src/SLCore/Listener/Branch/MatchSonarProjectBranchResponse.cs b/src/SLCore/Listener/Branch/MatchSonarProjectBranchResponse.cs index fb04eb0d4f..7017a789f3 100644 --- a/src/SLCore/Listener/Branch/MatchSonarProjectBranchResponse.cs +++ b/src/SLCore/Listener/Branch/MatchSonarProjectBranchResponse.cs @@ -20,4 +20,4 @@ namespace SonarLint.VisualStudio.SLCore.Listener.Branch; -public record MatchSonarProjectBranchResponse(string matchedSonarBranch); +public record MatchSonarProjectBranchResponse(string? matchedSonarBranch); diff --git a/src/SLCore/SLCoreStrings.Designer.cs b/src/SLCore/SLCoreStrings.Designer.cs index 08014bdeab..19e6f73bf3 100644 --- a/src/SLCore/SLCoreStrings.Designer.cs +++ b/src/SLCore/SLCoreStrings.Designer.cs @@ -230,6 +230,15 @@ public static string ConfigScope_UpdatedFileSystem { } } + /// + /// Looks up a localized string similar to Updated Sonar branch for Configuration Scope {0}: {1}. + /// + public static string ConfigScope_UpdatedSonarBranch { + get { + return ResourceManager.GetString("ConfigScope_UpdatedSonarBranch", resourceCulture); + } + } + /// /// Looks up a localized string similar to Configuration scope conflict. /// diff --git a/src/SLCore/SLCoreStrings.resx b/src/SLCore/SLCoreStrings.resx index 41c4a82bbc..e6557414c9 100644 --- a/src/SLCore/SLCoreStrings.resx +++ b/src/SLCore/SLCoreStrings.resx @@ -294,4 +294,7 @@ Updated analysis readiness for Configuration Scope {0}: {1} + + Updated Sonar branch for Configuration Scope {0}: {1} + \ No newline at end of file diff --git a/src/SLCore/State/ActiveConfigScopeTracker.cs b/src/SLCore/State/ActiveConfigScopeTracker.cs index 6b2d0d352e..9be1b7aa23 100644 --- a/src/SLCore/State/ActiveConfigScopeTracker.cs +++ b/src/SLCore/State/ActiveConfigScopeTracker.cs @@ -137,24 +137,28 @@ public void RemoveCurrentConfigScope() OnCurrentConfigurationScopeChanged(true); } - public bool TryUpdateRootOnCurrentConfigScope(string id, string root, string commandsBaseDir) - { - using (asyncLock.Acquire()) + public bool TryUpdateRootOnCurrentConfigScope(string? id, string root, string commandsBaseDir) => + UpdateCurrentScope(id, () => { - if (id is null || CurrentConfigScope?.Id != id) - { - return false; - } - - CurrentConfigScope = CurrentConfigScope with { RootPath = root, CommandsBaseDir = commandsBaseDir }; + CurrentConfigScope = CurrentConfigScope! with { RootPath = root, CommandsBaseDir = commandsBaseDir }; logger.WriteLine(SLCoreStrings.ConfigScope_UpdatedFileSystem, id, root, commandsBaseDir); - LogConfigurationScopeChangedUnsafe(); - } - OnCurrentConfigurationScopeChanged(false); - return true; - } + }); - public bool TryUpdateAnalysisReadinessOnCurrentConfigScope(string id, bool isReady) + public bool TryUpdateAnalysisReadinessOnCurrentConfigScope(string? id, bool isReady) => + UpdateCurrentScope(id, () => + { + CurrentConfigScope = CurrentConfigScope! with { IsReadyForAnalysis = isReady}; + logger.WriteLine(SLCoreStrings.ConfigScope_UpdatedAnalysisReadiness, id, isReady); + }); + + public bool TryUpdateMatchedBranchOnCurrentConfigScope(string? id, string branch) => + UpdateCurrentScope(id, () => + { + CurrentConfigScope = CurrentConfigScope! with { MatchedBranch = branch }; + logger.WriteLine(SLCoreStrings.ConfigScope_UpdatedSonarBranch, id, branch); + }); + + private bool UpdateCurrentScope(string? id, Action update) { using (asyncLock.Acquire()) { @@ -163,8 +167,7 @@ public bool TryUpdateAnalysisReadinessOnCurrentConfigScope(string id, bool isRea return false; } - CurrentConfigScope = CurrentConfigScope with { IsReadyForAnalysis = isReady}; - logger.WriteLine(SLCoreStrings.ConfigScope_UpdatedAnalysisReadiness, id, isReady); + update.Invoke(); LogConfigurationScopeChangedUnsafe(); } OnCurrentConfigurationScopeChanged(false); diff --git a/src/SLCore/State/ISlCoreGitChangeNotifier.cs b/src/SLCore/State/ISlCoreGitChangeNotifier.cs new file mode 100644 index 0000000000..03aa4dc6e2 --- /dev/null +++ b/src/SLCore/State/ISlCoreGitChangeNotifier.cs @@ -0,0 +1,25 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Core.Initialization; + +namespace SonarLint.VisualStudio.SLCore.State; + +public interface ISlCoreGitChangeNotifier : IRequireInitialization, IDisposable; diff --git a/src/SonarQube.Client.Tests/Helpers/ComponentKeyGeneratorTests.cs b/src/SonarQube.Client.Tests/Helpers/ComponentKeyGeneratorTests.cs deleted file mode 100644 index b8cf60fcef..0000000000 --- a/src/SonarQube.Client.Tests/Helpers/ComponentKeyGeneratorTests.cs +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SonarQube.Client.Helpers; - -namespace SonarQube.Client.Tests.Helpers; - -[TestClass] -public class ComponentKeyGeneratorTests -{ - [DataTestMethod] - [DataRow(@"c:\dir\dir\root\dir\file.cs", @"c:\dir\dir\root\", "ProjectKey", "ProjectKey:dir/file.cs")] - [DataRow(@"c:\dir\dir\root\file.cs", @"c:\dir\dir\root\", "ProjectKey", "ProjectKey:file.cs")] - [DataRow(@"c:\dir\dir\root\dir1\dir2\file.cs", @"c:\dir\dir\root\", "Project:Key", "Project:Key:dir1/dir2/file.cs")] - public void GetComponentKey(string filePath, string rootPath, string projectName, string expected) - { - ComponentKeyGenerator.GetComponentKey(filePath, rootPath, projectName).Should().Be(expected); - } - - [TestMethod] - public void GetComponentKey_NonRootedPath_Throws() - { - Action act = () => { ComponentKeyGenerator.GetComponentKey(@"c:\dir\dir\root\dir\file.cs", @".\non\rooted\", "project"); }; - - act.Should().ThrowExactly().WithMessage("Invalid root path format"); - } - - [TestMethod] - public void GetComponentKey_NonFolderPath_Throws() - { - Action act = () => { ComponentKeyGenerator.GetComponentKey(@"c:\dir\dir\root\dir\file.cs", @"c:\not\folder", "project"); }; - - act.Should().ThrowExactly().WithMessage("Invalid root path format"); - } - - [TestMethod] - public void GetComponentKey_NonMatchingRoot_Throws() - { - Action act = () => { ComponentKeyGenerator.GetComponentKey(@"c:\dir\dir\root\dir\file.cs", @"c:\not\same\folder\", "project"); }; - - act.Should().ThrowExactly().WithMessage("Local path is not under this root"); - } -} diff --git a/src/SonarQube.Client.Tests/Helpers/SonarQubeIssueSeverityConverterTests.cs b/src/SonarQube.Client.Tests/Helpers/SonarQubeIssueSeverityConverterTests.cs deleted file mode 100644 index d0be76ba7c..0000000000 --- a/src/SonarQube.Client.Tests/Helpers/SonarQubeIssueSeverityConverterTests.cs +++ /dev/null @@ -1,52 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Tests.Helpers -{ - [TestClass] - public class SonarQubeIssueSeverityConverterTests - { - [TestMethod] - // Exact matches - [DataRow("BLOCKER", SonarQubeIssueSeverity.Blocker)] - [DataRow("CRITICAL", SonarQubeIssueSeverity.Critical)] - [DataRow("INFO", SonarQubeIssueSeverity.Info)] - [DataRow("MAJOR ", SonarQubeIssueSeverity.Major)] - [DataRow("MINOR", SonarQubeIssueSeverity.Minor)] - - // Case-insensitivity - [DataRow("blocker", SonarQubeIssueSeverity.Blocker)] - [DataRow("minOR", SonarQubeIssueSeverity.Minor)] - - // Unknowns - [DataRow(null, SonarQubeIssueSeverity.Unknown)] - [DataRow("", SonarQubeIssueSeverity.Unknown)] - [DataRow("MAJORXXX", SonarQubeIssueSeverity.Unknown)] - [DataRow("foo", SonarQubeIssueSeverity.Unknown)] - public void SeverityConversion(string inputData, SonarQubeIssueSeverity expectedResult) - { - SonarQubeIssueSeverityConverter.Convert(inputData).Should().Be(expectedResult); - } - } -} diff --git a/src/SonarQube.Client.Tests/Helpers/SonarQubeIssueTypeConverterTests.cs b/src/SonarQube.Client.Tests/Helpers/SonarQubeIssueTypeConverterTests.cs deleted file mode 100644 index 0f35d37a34..0000000000 --- a/src/SonarQube.Client.Tests/Helpers/SonarQubeIssueTypeConverterTests.cs +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Tests.Helpers -{ - [TestClass] - public class SonarQubeIssueTypeConverterTests - { - [TestMethod] - // Exact matches - [DataRow("CODE_SMELL", SonarQubeIssueType.CodeSmell)] - [DataRow("BUG", SonarQubeIssueType.Bug)] - [DataRow("VULNERABILITY", SonarQubeIssueType.Vulnerability)] - [DataRow("SECURITY_HOTSPOT", SonarQubeIssueType.SecurityHotspot)] - - // Case-insensitivity - [DataRow("bug", SonarQubeIssueType.Bug)] - [DataRow("security_hotspot", SonarQubeIssueType.SecurityHotspot)] - - // Unknowns - [DataRow(null, SonarQubeIssueType.Unknown)] - [DataRow("", SonarQubeIssueSeverity.Unknown)] - [DataRow("BUGX", SonarQubeIssueSeverity.Unknown)] - [DataRow("foo", SonarQubeIssueSeverity.Unknown)] - public void Convert(string inputData, SonarQubeIssueType expectedResult) - { - SonarQubeIssueTypeConverter.Convert(inputData).Should().Be(expectedResult); - } - } -} diff --git a/src/SonarQube.Client.Tests/IssuesProtobufResponse b/src/SonarQube.Client.Tests/IssuesProtobufResponse deleted file mode 100644 index b2582e9d8c..0000000000 Binary files a/src/SonarQube.Client.Tests/IssuesProtobufResponse and /dev/null differ diff --git a/src/SonarQube.Client.Tests/Models/IssueFlowTests.cs b/src/SonarQube.Client.Tests/Models/IssueFlowTests.cs deleted file mode 100644 index 56b7cb6b6c..0000000000 --- a/src/SonarQube.Client.Tests/Models/IssueFlowTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Generic; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Tests.Models -{ - [TestClass] - public class IssueFlowTests - { - [TestMethod] - public void Ctor_LocationsAreNeverNull() - { - var testSubject = new IssueFlow(null); - - testSubject.Locations.Should().BeEmpty(); - } - - [TestMethod] - public void Ctor_PropertiesAreSet() - { - var locations = new List - { - new IssueLocation("file1", "component1", null, "message1"), - new IssueLocation("file2", "component2", null, "message2") - }; - - var testSubject = new IssueFlow(locations); - - testSubject.Locations.Should().BeEquivalentTo(locations[0], locations[1]); - } - } -} diff --git a/src/SonarQube.Client.Tests/Models/ServerExclusionsTests.cs b/src/SonarQube.Client.Tests/Models/ServerExclusionsTests.cs deleted file mode 100644 index a678e75b3c..0000000000 --- a/src/SonarQube.Client.Tests/Models/ServerExclusionsTests.cs +++ /dev/null @@ -1,233 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Generic; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Tests.Models -{ - [TestClass] - public class ServerExclusionsTests - { - [TestMethod] - public void Equals_OtherObjIsNull_False() - { - var testSubject = new ServerExclusions( - exclusions: new[] { "exclusion1" }, - globalExclusions: new[] { "global" }, - inclusions: new[] { "inclusion1" }); - - testSubject.Equals(null).Should().BeFalse(); - } - - [TestMethod] - public void Equals_ExclusionsAreDifferent_False() - { - var testSubject = new ServerExclusions( - exclusions: new[] { "exclusion1" }, - globalExclusions: new[] { "global1" }, - inclusions: new[] { "inclusion1" }); - - var other = new ServerExclusions( - exclusions: new[] { "exclusion2" }, - globalExclusions: new[] { "global1" }, - inclusions: new[] { "inclusion1" }); - - testSubject.Equals(other).Should().BeFalse(); - } - - [TestMethod] - public void Equals_GlobalExclusionsAreDifferent_False() - { - var testSubject = new ServerExclusions( - exclusions: new[] { "exclusion1" }, - globalExclusions: new[] { "global1" }, - inclusions: new[] { "inclusion1" }); - - var other = new ServerExclusions( - exclusions: new[] { "exclusion1" }, - globalExclusions: new[] { "global2" }, - inclusions: new[] { "inclusion1" }); - - testSubject.Equals(other).Should().BeFalse(); - } - - [TestMethod] - public void Equals_InclusionsAreDifferent_False() - { - var testSubject = new ServerExclusions( - exclusions: new[] { "exclusion1" }, - globalExclusions: new[] { "global1" }, - inclusions: new[] { "inclusion1" }); - - var other = new ServerExclusions( - exclusions: new[] { "exclusion1" }, - globalExclusions: new[] { "global1" }, - inclusions: new[] { "inclusion2" }); - - testSubject.Equals(other).Should().BeFalse(); - } - - [TestMethod] - public void Equals_EverythingIsTheSame_True() - { - var testSubject = new ServerExclusions( - exclusions: new[] { "exclusion1" }, - globalExclusions: new[] { "global1" }, - inclusions: new[] { "inclusion1" }); - - var other = new ServerExclusions( - exclusions: new[] { "exclusion1" }, - globalExclusions: new[] { "global1" }, - inclusions: new[] { "inclusion1" }); - - testSubject.Equals(other).Should().BeTrue(); - } - - [TestMethod] - public void Equals_SameReference_True() - { - var testSubject = new ServerExclusions( - exclusions: new[] { "exclusion1" }, - globalExclusions: new[] { "global" }, - inclusions: new[] { "inclusion1" }); - - testSubject.Equals(testSubject).Should().BeTrue(); - } - - [TestMethod] - public void Ctor_HasExclusions_AppendPathPrefix() - { - var testSubject = new ServerExclusions( - exclusions: new[] { "path1", "**\\path2", "**/path3", "**path4", "*/*" }, - globalExclusions: Array.Empty(), - inclusions: null); - - testSubject.Exclusions.Should().BeEquivalentTo( - "**/path1", - "**/**\\path2", - "**/path3", - "**/**path4", - "**/*/*"); - testSubject.GlobalExclusions.Should().BeEmpty(); - testSubject.Inclusions.Should().BeEmpty(); - } - - [TestMethod] - public void Ctor_HasGlobalExclusions_AppendPathPrefix() - { - var testSubject = new ServerExclusions( - exclusions: null, - globalExclusions: new[] { "path1", "**\\path2", "**/path3", "**path4", "*/*" }, - inclusions: Array.Empty()); - - testSubject.Exclusions.Should().BeEmpty(); - testSubject.GlobalExclusions.Should().BeEquivalentTo( - "**/path1", - "**/**\\path2", - "**/path3", - "**/**path4", - "**/*/*"); - testSubject.Inclusions.Should().BeEmpty(); - } - - [TestMethod] - public void Ctor_HasInclusions_AppendPathPrefix() - { - var testSubject = new ServerExclusions( - exclusions: Array.Empty(), - globalExclusions: null, - inclusions: new[] {"path1", "**\\path2", "**/path3", "**path4", "*/*"}); - - testSubject.Exclusions.Should().BeEmpty(); - testSubject.GlobalExclusions.Should().BeEmpty(); - testSubject.Inclusions.Should().BeEquivalentTo( - "**/path1", - "**/**\\path2", - "**/path3", - "**/**path4", - "**/*/*"); - } - - [TestMethod] - public void ToDictionary_HasExclusions_ReturnsConcatenatedValues() - { - var testSubject = new ServerExclusions( - exclusions: new[] { "**/path1", "**/*/path2" }, - globalExclusions: new[] { "**/path1" }, - inclusions: null); - - var result = testSubject.ToDictionary(); - - result.Should().BeEquivalentTo( - new Dictionary - { - {"sonar.exclusions", "**/path1,**/*/path2"}, - {"sonar.global.exclusions", "**/path1"}, - {"sonar.inclusions", ""} - }); - } - - [TestMethod] - public void ToDictionary_HasGlobalExclusions_ReturnsConcatenatedValues() - { - var testSubject = new ServerExclusions( - exclusions: null, - globalExclusions: new[] { "**/path1", "**/*/path2" }, - inclusions: new[] { "**/path1" }); - - var result = testSubject.ToDictionary(); - - result.Should().BeEquivalentTo( - new Dictionary - { - {"sonar.exclusions", ""}, - {"sonar.global.exclusions", "**/path1,**/*/path2"}, - {"sonar.inclusions", "**/path1"} - }); - } - - [TestMethod] - public void ToDictionary_HasInclusions_ReturnsConcatenatedValues() - { - var testSubject = new ServerExclusions( - exclusions: new[] {"**/path1"}, - globalExclusions: null, - inclusions: new[] {"**/path1", "**/*/path2"}); - - var result = testSubject.ToDictionary(); - - result.Should().BeEquivalentTo( - new Dictionary - { - {"sonar.exclusions", "**/path1"}, - {"sonar.global.exclusions", ""}, - {"sonar.inclusions", "**/path1,**/*/path2"} - }); - } - - - - - } -} diff --git a/src/SonarQube.Client.Tests/Models/ServerSentEvents/BranchAndIssueKeyTests.cs b/src/SonarQube.Client.Tests/Models/ServerSentEvents/BranchAndIssueKeyTests.cs deleted file mode 100644 index 2b0e352913..0000000000 --- a/src/SonarQube.Client.Tests/Models/ServerSentEvents/BranchAndIssueKeyTests.cs +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarQube.Client.Tests.Models.ServerSentEvents -{ - [TestClass] - public class BranchAndIssueKeyTests - { - [TestMethod] - public void Ctor_InvalidKey_Throws() - { - Action act = () => { new BranchAndIssueKey(null, "main"); }; - - act.Should().ThrowExactly().And.ParamName.Should().Be("issueKey"); - } - - [TestMethod] - public void Ctor_InvalidBranch_Throws() - { - Action act = () => { new BranchAndIssueKey("id1", null); }; - - act.Should().ThrowExactly().And.ParamName.Should().Be("branchName"); - } - } -} diff --git a/src/SonarQube.Client.Tests/Models/ServerSentEvents/ClientContract/IssueChangedServerEventTests.cs b/src/SonarQube.Client.Tests/Models/ServerSentEvents/ClientContract/IssueChangedServerEventTests.cs deleted file mode 100644 index 8529cf72f1..0000000000 --- a/src/SonarQube.Client.Tests/Models/ServerSentEvents/ClientContract/IssueChangedServerEventTests.cs +++ /dev/null @@ -1,67 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarQube.Client.Tests.Models.ServerSentEvents.ClientContract -{ - [TestClass] - public class IssueChangedServerEventTests - { - [TestMethod] - public void Ctor_NullIssuesArray_Throws() - { - // The implementation of ToString assumes the issues array is not null, - // so we'll check that the constructor actually enforces this - Action act = () => new IssueChangedServerEvent("any", true, null); - act.Should().Throw().And.ParamName.Should().Be("issues"); - } - - [TestMethod] - public void ToString_EmptyList_ContainsExpectedStrings() - { - var testSubject = new IssueChangedServerEvent("projectKey A", true, Array.Empty()); - - var actual = testSubject.ToString(); - - actual.Should().ContainAll("projectKey A", "True", "0"); - } - - [TestMethod] - public void ToString_MultipleItemsInList_ContainsExpectedStrings() - { - var testSubject = new IssueChangedServerEvent("projectKey B", false, - new BranchAndIssueKey[] - { - new BranchAndIssueKey("issuekey1", "branch1"), - new BranchAndIssueKey("issuekey2", "branch2") - }); - - var actual = testSubject.ToString(); - - actual.Should().ContainAll("projectKey B", "False", "2", - "issuekey1", "branch1", - "issuekey2", "branch2"); - } - } -} diff --git a/src/SonarQube.Client.Tests/Models/ServerSentEvents/IssueChangedServerEventTests.cs b/src/SonarQube.Client.Tests/Models/ServerSentEvents/IssueChangedServerEventTests.cs deleted file mode 100644 index 917c1897da..0000000000 --- a/src/SonarQube.Client.Tests/Models/ServerSentEvents/IssueChangedServerEventTests.cs +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarQube.Client.Tests.Models.ServerSentEvents -{ - [TestClass] - public class IssueChangedServerEventTests - { - [TestMethod] - public void Ctor_InvalidIssuesList_Throws() - { - Action act = () => { new IssueChangedServerEvent("MyProject", false, null); }; - - act.Should().ThrowExactly().And.ParamName.Should().Be("issues"); - } - - [TestMethod] - public void Ctor_InvalidProjectKey_Throws() - { - Action act = () => { new IssueChangedServerEvent(null, false, new[]{new BranchAndIssueKey("i", "b")}); }; - - act.Should().ThrowExactly().And.ParamName.Should().Be("projectKey"); - } - } -} diff --git a/src/SonarQube.Client.Tests/Models/ServerSentEvents/SSEStreamReaderTests.cs b/src/SonarQube.Client.Tests/Models/ServerSentEvents/SSEStreamReaderTests.cs deleted file mode 100644 index cf1b89a651..0000000000 --- a/src/SonarQube.Client.Tests/Models/ServerSentEvents/SSEStreamReaderTests.cs +++ /dev/null @@ -1,186 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using Moq; -using Newtonsoft.Json; -using SonarQube.Client.Logging; -using SonarQube.Client.Models.ServerSentEvents; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; -using SonarQube.Client.Models.ServerSentEvents.ServerContract; -using SonarQube.Client.Tests.Infra; - -namespace SonarQube.Client.Tests.Models.ServerSentEvents -{ - [TestClass] - public class SSEStreamReaderTests - { - [TestMethod] - public void ReadAsync_UnderlyingReaderException_LogsDisposesAndThrows() - { - var streamReader = new Mock(); - var exceptionMessage = "Some network error"; - streamReader.Setup(reader => reader.ReadAsync()).Throws(new Exception(exceptionMessage)); - - var testSubject = CreateTestSubject(streamReader.Object); - - Func act = () => testSubject.ReadAsync(); - - act.Should().Throw().Which.Message.Should().Be(exceptionMessage); - streamReader.Verify(reader => reader.Dispose(), Times.Once); - } - - [TestMethod] - public async Task ReadAsync_Null_NullReturned() - { - var sqSSEStreamReader = CreateSqStreamReader((ISqServerEvent)null); - - var testSubject = CreateTestSubject(sqSSEStreamReader); - - var result = await testSubject.ReadAsync(); - - result.Should().BeNull(); - } - - [TestMethod] - [Description("SQ stream events that we do not support yet. We need to ignore them.")] - public async Task ReadAsync_UnrecognizedEventType_NullReturned() - { - var sqSSEStreamReader = CreateSqStreamReader(new SqServerEvent("some type 111", "some data")); - - var testSubject = CreateTestSubject(sqSSEStreamReader); - - var result = await testSubject.ReadAsync(); - - result.Should().BeNull(); - } - - [TestMethod] - public async Task ReadAsync_FailureToDeserializeTheEventData_ExceptionLoggedAndNullReturned() - { - var sqSSEStreamReader = CreateSqStreamReader(new SqServerEvent("IssueChanged", "some invalid data")); - var logger = new TestLogger(); - - var testSubject = CreateTestSubject(sqSSEStreamReader, logger); - - var result = await testSubject.ReadAsync(); - - result.Should().BeNull(); - - logger.DebugMessages.Should().Contain(x => - x.Contains(nameof(JsonReaderException)) && - x.Contains("IssueChanged") && - x.Contains("some invalid data")); - } - - [TestMethod, Description("Missing mandatory 'branchName' field")] - public async Task ReadAsync_IssueChangedEventType_MissingMandatoryFields_ExceptionLoggedAndNullReturned() - { - const string serializedIssueChangedEvent = - "{\"projectKey\": \"projectKey1\",\"issues\": [{\"issueKey\": \"key1\"}],\"resolved\": \"true\"}"; - - var sqSSEStreamReader = CreateSqStreamReader(new SqServerEvent("IssueChanged", serializedIssueChangedEvent)); - var logger = new TestLogger(); - - var testSubject = CreateTestSubject(sqSSEStreamReader, logger); - - var result = await testSubject.ReadAsync(); - - result.Should().BeNull(); - - logger.DebugMessages.Should().Contain(x => - x.Contains(nameof(ArgumentNullException)) && - x.Contains("branchName") && - x.Contains("IssueChanged") && - x.Contains("projectKey1")); - } - - [TestMethod] - public async Task ReadAsync_IssueChangedEventType_DeserializedEvent() - { - const string serializedIssueChangedEvent = - "{\"projectKey\": \"projectKey1\",\"issues\": [{\"issueKey\": \"key1\",\"branchName\": \"master\"}],\"resolved\": \"true\"}"; - - var sqSSEStreamReader = CreateSqStreamReader(new SqServerEvent("IssueChanged", serializedIssueChangedEvent)); - - var testSubject = CreateTestSubject(sqSSEStreamReader); - - var result = await testSubject.ReadAsync(); - - result.Should().NotBeNull(); - result.Should().BeOfType(); - result.Should().BeEquivalentTo( - new IssueChangedServerEvent( - projectKey: "projectKey1", - isResolved: true, - issues: new[] { new BranchAndIssueKey("key1", "master") })); - } - - [TestMethod] - public async Task ReadAsync_QualityProfileEventType_DeserializedEvent() - { - const string serializedQualityProfileEvent = """ - { - "projects": [ - "ABC" - ], - "activatedRules": [ - { - "key": "javascript:S4139", - "language": "js", - "severity": "MAJOR", - "params": [] - } - ], - "deactivatedRules": [] - } - """; - - var sqSSEStreamReader = CreateSqStreamReader(new SqServerEvent("RuleSetChanged", serializedQualityProfileEvent)); - - var testSubject = CreateTestSubject(sqSSEStreamReader); - - var result = await testSubject.ReadAsync(); - - result.Should().NotBeNull(); - result.Should().BeOfType(); - // note: event implementation is empty, no test for data validity here either - } - - private ISqSSEStreamReader CreateSqStreamReader(params ISqServerEvent[] events) - { - var streamReader = new Mock(); - var sequenceSetup = streamReader.SetupSequence(x => x.ReadAsync()); - - foreach (var sqServerEvent in events) - { - sequenceSetup.ReturnsAsync(sqServerEvent); - } - - return streamReader.Object; - } - - private SSEStreamReader CreateTestSubject(ISqSSEStreamReader sqSSEStreamReader, ILogger logger = null) - { - logger ??= Mock.Of(); - - return new SSEStreamReader(sqSSEStreamReader, logger); - } - } -} diff --git a/src/SonarQube.Client.Tests/Models/ServerSentEvents/SqSSEStreamReaderTests.cs b/src/SonarQube.Client.Tests/Models/ServerSentEvents/SqSSEStreamReaderTests.cs deleted file mode 100644 index 590e931963..0000000000 --- a/src/SonarQube.Client.Tests/Models/ServerSentEvents/SqSSEStreamReaderTests.cs +++ /dev/null @@ -1,171 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.IO; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using SonarQube.Client.Models.ServerSentEvents.ServerContract; - -namespace SonarQube.Client.Tests.Models.ServerSentEvents -{ - [TestClass] - public class SqSSEStreamReaderTests - { - [TestMethod, Timeout(10000)] - [DataRow("")] - [DataRow("some data\nanother data")] - public async Task ReadAsync_EndOfStream_TaskFinishes(string content) - { - var networkStreamReader = CreateNetworkStreamReader(content); - - var testSubject = CreateTestSubject(networkStreamReader); - - var result = await testSubject.ReadAsync(); - - result.Should().BeNull(); - networkStreamReader.EndOfStream.Should().BeTrue(); - } - - [TestMethod, Timeout(10000)] - public async Task ReadAsync_TokenIsCancelledBeforeStreamIsFinished_TaskFinishes() - { - var networkStreamReader = CreateNetworkStreamReader(content: "some data\nanother data\n"); - var cancellationToken = new CancellationTokenSource(); - cancellationToken.Cancel(); - - var testSubject = CreateTestSubject(networkStreamReader, token: cancellationToken.Token); - - var result = await testSubject.ReadAsync(); - - result.Should().BeNull(); - networkStreamReader.EndOfStream.Should().BeFalse(); - } - - [TestMethod, Timeout(10000)] - public async Task ReadAsync_StreamLinesAreAggregatedUntilAnEmptyLine() - { - var networkStreamReader = CreateNetworkStreamReader(content: "line 1\nline 2\nline 3\n\nline 4\nline 5\n\nline 6\nline 7\n"); - var parsedEvent1 = Mock.Of(); - var parsedEvent2 = Mock.Of(); - - var sqServerSentEventParser = new Mock(); - - sqServerSentEventParser - .Setup(x => x.Parse(new[] {"line 1", "line 2", "line 3"})) - .Returns(parsedEvent1); - - sqServerSentEventParser - .Setup(x => x.Parse(new[] { "line 4", "line 5" })) - .Returns(parsedEvent2); - - var testSubject = CreateTestSubject(networkStreamReader, sqServerSentEventParser.Object); - - var actualEvent1 = await testSubject.ReadAsync(); - actualEvent1.Should().Be(parsedEvent1); - networkStreamReader.EndOfStream.Should().BeFalse(); - - var actualEvent2 = await testSubject.ReadAsync(); - actualEvent2.Should().Be(parsedEvent2); - networkStreamReader.EndOfStream.Should().BeFalse(); - - var actualEvent3 = await testSubject.ReadAsync(); - actualEvent3.Should().BeNull(); - networkStreamReader.EndOfStream.Should().BeTrue(); - - sqServerSentEventParser.VerifyAll(); - sqServerSentEventParser.VerifyNoOtherCalls(); - } - - [TestMethod, Timeout(10000)] - public async Task ReadAsync_FailureToParseAnEvent_EventIsIgnored() - { - var networkStreamReader = CreateNetworkStreamReader(content: "line 1\n\nline 2\nline 3\n\n"); - var sqServerSentEventParser = new Mock(); - - sqServerSentEventParser - .Setup(x => x.Parse(new[] { "line 1" })) - .Returns((ISqServerEvent) null); - - var parsedEvent = Mock.Of(); - - sqServerSentEventParser - .Setup(x => x.Parse(new[] { "line 2", "line 3" })) - .Returns(parsedEvent); - - var testSubject = CreateTestSubject(networkStreamReader, sqServerSentEventParser.Object); - - var actualEvent = await testSubject.ReadAsync(); - actualEvent.Should().Be(parsedEvent); - networkStreamReader.EndOfStream.Should().BeTrue(); - - sqServerSentEventParser.VerifyAll(); - sqServerSentEventParser.VerifyNoOtherCalls(); - } - - [TestMethod, Timeout(10000)] - public async Task ReadAsync_StreamCrashesInTheMiddle_Exception() - { - var networkStreamReader = CreateNetworkStreamReader(content: "line 1\n\nline 2\nline 3\n\n"); - var cancellationToken = new CancellationTokenSource(); - cancellationToken.Cancel(); - - var testSubject = CreateTestSubject(networkStreamReader, token: cancellationToken.Token); - - await testSubject.ReadAsync(); - networkStreamReader.Close(); - - Func> func = async () => await testSubject.ReadAsync(); - - func.Should().ThrowExactly().And.Message.Should().Be("Cannot read from a closed TextReader."); - } - - [TestMethod] - public void Dispose_ClosesStream() - { - var networkStreamReader = CreateNetworkStreamReader(content: "content"); - - var testSubject = CreateTestSubject(networkStreamReader); - - networkStreamReader.BaseStream.Should().NotBeNull(); - - testSubject.Dispose(); - - networkStreamReader.BaseStream.Should().BeNull(); - } - - private static StreamReader CreateNetworkStreamReader(string content) => - new(new MemoryStream(Encoding.UTF8.GetBytes(content))); - - private static SqSSEStreamReader CreateTestSubject( - StreamReader networkStreamReader, - ISqServerSentEventParser sqServerSentEventParser = null, - CancellationToken? token = null) - { - token ??= CancellationToken.None; - - return new SqSSEStreamReader(networkStreamReader, token.Value, sqServerSentEventParser); - } - } -} diff --git a/src/SonarQube.Client.Tests/Models/ServerSentEvents/SqServerSentEventParserTests.cs b/src/SonarQube.Client.Tests/Models/ServerSentEvents/SqServerSentEventParserTests.cs deleted file mode 100644 index 662372b34d..0000000000 --- a/src/SonarQube.Client.Tests/Models/ServerSentEvents/SqServerSentEventParserTests.cs +++ /dev/null @@ -1,191 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SonarQube.Client.Models.ServerSentEvents.ServerContract; - -namespace SonarQube.Client.Tests.Models.ServerSentEvents -{ - [TestClass] - public class SqServerSentEventParserTests - { - [TestMethod] - public void Parse_NullEventLines_Null() - { - var testSubject = CreateTestSubject(); - - var result = testSubject.Parse(null); - - result.Should().BeNull(); - } - - [TestMethod] - public void Parse_EmptyEventLines_Null() - { - var testSubject = CreateTestSubject(); - - var result = testSubject.Parse(Array.Empty()); - - result.Should().BeNull(); - } - - [TestMethod] - public void Parse_InvalidEventType_MissingEventTypeField_Null() - { - var testSubject = CreateTestSubject(); - - var result = testSubject.Parse(new[] {"data: some data"}); - - result.Should().BeNull(); - } - - [TestMethod] - public void Parse_InvalidEventType_EventTypeIsEmpty_Null() - { - var testSubject = CreateTestSubject(); - - var result = testSubject.Parse(new[] { "event: ", "data: some data"}); - - result.Should().BeNull(); - } - - [TestMethod] - public void Parse_InvalidEventType_EventTypeIsNotInCorrectFormat_Null() - { - var testSubject = CreateTestSubject(); - - var result = testSubject.Parse(new[] { "event : extra space", "data: some data" }); - - result.Should().BeNull(); - } - - [TestMethod] - public void Parse_InvalidEventData_MissingEventDataField_Null() - { - var testSubject = CreateTestSubject(); - - var result = testSubject.Parse(new[]{ "event: some type" }); - - result.Should().BeNull(); - } - - [TestMethod] - public void Parse_InvalidEventData_EventDataIsEmpty_Null() - { - var testSubject = CreateTestSubject(); - - var result = testSubject.Parse(new[] { "event: some type", "data: " }); - - result.Should().BeNull(); - } - - [TestMethod] - public void Parse_InvalidEventData_EventDataIsNotInCorrectFormat_Null() - { - var testSubject = CreateTestSubject(); - - var result = testSubject.Parse(new[] { "event: some type", "data : extra space" }); - - result.Should().BeNull(); - } - - - [TestMethod] - public void Parse_CorrectEventString_ParsedEvent() - { - var eventLines = new[] - { - "event: some event type", - "data: some event data" - }; - - var testSubject = CreateTestSubject(); - - var result = testSubject.Parse(eventLines); - - result.Should().NotBeNull(); - result.Type.Should().Be("some event type"); - result.Data.Should().Be("some event data"); - } - - [TestMethod] - public void Parse_CorrectEventString_MultilineData_ParsedEvent() - { - var eventLines = new[] - { - "event: some event type", - "data: some event data1", - "data: ", - "data: some event data2", - }; - - var testSubject = CreateTestSubject(); - - var result = testSubject.Parse(eventLines); - - result.Should().NotBeNull(); - result.Type.Should().Be("some event type"); - result.Data.Should().Be("some event data1some event data2"); - } - - [TestMethod] - public void Parse_HasJunkFields_JunkFieldsIgnored() - { - var eventLines = new[] - { - "junk1: junk field1", - "EVENT: junk event type", - "event:", - "event: some event type", - "data: ", - "data: some event data1", - "junk2: junk field2", - "DATA: junk data2", - "data: some event data2", - "junk3: junk field3" - }; - - var testSubject = CreateTestSubject(); - - var result = testSubject.Parse(eventLines); - - result.Should().NotBeNull(); - result.Type.Should().Be("some event type"); - result.Data.Should().Be("some event data1some event data2"); - } - - [TestMethod] - public void Parse_EventTypeIsNotTheFirstField_EventTypeIsStillParsedCorrectly() - { - var testSubject = CreateTestSubject(); - - var result = testSubject.Parse(new[] { "data: some data", "event: some type" }); - - result.Should().NotBeNull(); - result.Type.Should().Be("some type"); - result.Data.Should().Be("some data"); - } - - - private static SqServerSentEventParser CreateTestSubject() => new(); - } -} diff --git a/src/SonarQube.Client.Tests/Models/SonarQubeIssueTests.cs b/src/SonarQube.Client.Tests/Models/SonarQubeIssueTests.cs deleted file mode 100644 index bb0e499848..0000000000 --- a/src/SonarQube.Client.Tests/Models/SonarQubeIssueTests.cs +++ /dev/null @@ -1,89 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Generic; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Tests.Models -{ - [TestClass] - public class SonarQubeIssueTests - { - private static readonly DateTimeOffset ValidTimestamp = DateTimeOffset.UtcNow; - - [TestMethod] - public void Ctor_FilePathCanBeNull() - { - var testSubject = new SonarQubeIssue("issueKey", null, "hash", "message", "module", "rule", true, - SonarQubeIssueSeverity.Info, ValidTimestamp, ValidTimestamp, textRange: null, flows: null); - - testSubject.FilePath.Should().BeNull(); - } - - [TestMethod] - public void Ctor_TextRangeCanBeNull() - { - var testSubject = new SonarQubeIssue("issueKey", "file", "hash", "message", "module", "rule", true, - SonarQubeIssueSeverity.Info, ValidTimestamp, ValidTimestamp, textRange: null, flows: null); - - testSubject.TextRange.Should().BeNull(); - } - - [TestMethod] - public void Ctor_FlowsAreNeverNull() - { - var testSubject = new SonarQubeIssue("issueKey", "file", "hash", "message", "module", "rule", true, - SonarQubeIssueSeverity.Info, ValidTimestamp, ValidTimestamp, new IssueTextRange(123, 456, 7, 8), flows: null); - - testSubject.Flows.Should().BeEmpty(); - } - - [TestMethod] - public void Ctor_PropertiesAreSet() - { - var creationTimestamp = DateTimeOffset.Parse("2001-12-13T10:11:12+0000"); - var lastUpdateTimestamp = DateTimeOffset.Parse("2020-01-02T13:14:15+0000"); - - var flows = new List - { - new IssueFlow(null), new IssueFlow(null) - }; - var testSubject = new SonarQubeIssue("issueKey", "file", "hash", "message", "module", "rule", true, SonarQubeIssueSeverity.Info, - creationTimestamp, lastUpdateTimestamp, new IssueTextRange(123, 456, 7, 8), flows, "contextKey"); - - testSubject.IssueKey.Should().Be("issueKey"); - testSubject.FilePath.Should().Be("file"); - testSubject.Hash.Should().Be("hash"); - testSubject.Message.Should().Be("message"); - testSubject.ModuleKey.Should().Be("module"); - testSubject.RuleId.Should().Be("rule"); - testSubject.IsResolved.Should().BeTrue(); - testSubject.Severity.Should().Be(SonarQubeIssueSeverity.Info); - testSubject.CreationTimestamp.Should().Be(creationTimestamp); - testSubject.LastUpdateTimestamp.Should().Be(lastUpdateTimestamp); - testSubject.TextRange.Should().BeEquivalentTo(new IssueTextRange(123, 456, 7, 8)); - testSubject.Flows.Should().BeEquivalentTo(flows); - testSubject.Context.Should().Be("contextKey"); - } - } -} diff --git a/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetExclusionsRequestTests.cs b/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetExclusionsRequestTests.cs deleted file mode 100644 index 3027a49240..0000000000 --- a/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetExclusionsRequestTests.cs +++ /dev/null @@ -1,144 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Net.Http; -using Moq; -using SonarQube.Client.Api.V7_20; -using SonarQube.Client.Tests.Infra; -using static SonarQube.Client.Tests.Infra.MocksHelper; - -namespace SonarQube.Client.Tests.Requests.Api.V7_20 -{ - [TestClass] - public class GetExclusionsRequestTests - { - [TestMethod] - [DataRow("")] - [DataRow(@"{}")] - [DataRow(@"{""settings"": []}")] - [DataRow(@"{""settings"": [{""key"": ""some.other.setting"",""values"": [""val1""]}]}")] - public async Task InvokeAsync_AllSettingAreMissing_ReturnsEmptyConfiguration(string response) - { - const string projectKey = "myproject"; - - var testSubject = CreateTestSubject(projectKey); - - var handlerMock = new Mock(MockBehavior.Strict); - var httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri(ValidBaseAddress) }; - - var request = $"api/settings/values?component={projectKey}&keys=sonar.exclusions%2Csonar.global.exclusions%2Csonar.inclusions"; - - SetupHttpRequest(handlerMock, request, response); - - var result = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - result.Should().NotBeNull(); - - result.Exclusions.Should().BeEmpty(); - result.GlobalExclusions.Should().BeEmpty(); - result.Inclusions.Should().BeEmpty(); - } - - [TestMethod] - [Description("This is what SonarCloud returns when the setting is not defined")] - public async Task InvokeAsync_SomeMissingSetting_ReturnsDefinedProperties() - { - const string projectKey = "myproject"; - - var testSubject = CreateTestSubject(projectKey); - - var handlerMock = new Mock(MockBehavior.Strict); - var httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri(ValidBaseAddress) }; - - var request = $"api/settings/values?component={projectKey}&keys=sonar.exclusions%2Csonar.global.exclusions%2Csonar.inclusions"; - var response = @"{ - ""settings"": [ - { - ""key"": ""sonar.global.exclusions"", - ""values"": [ - ""**/build-wrapper-dump.json"" - ] - } - ] -}"; - - SetupHttpRequest(handlerMock, request, response); - - var result = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - result.Should().NotBeNull(); - - result.Exclusions.Should().BeEmpty(); - result.GlobalExclusions.Should().BeEquivalentTo("**/build-wrapper-dump.json"); - result.Inclusions.Should().BeEmpty(); - } - - [TestMethod] - public async Task InvokeAsync_ExistingSetting_ReturnsDefinedProperties() - { - const string projectKey = "myproject"; - - var testSubject = CreateTestSubject(projectKey); - - var handlerMock = new Mock(MockBehavior.Strict); - var httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri(ValidBaseAddress) }; - - var request = $"api/settings/values?component={projectKey}&keys=sonar.exclusions%2Csonar.global.exclusions%2Csonar.inclusions"; - var response = @"{ - ""settings"": [ - { - ""key"": ""sonar.exclusions"", - ""values"": [ - ""**/value1"", - ""value2"", - ""some/value/3"", - ] - }, - { - ""key"": ""sonar.global.exclusions"", - ""values"": [ - ""some/value/4"", - ] - }, - { - ""key"": ""sonar.inclusions"", - ""values"": [ - ""**/111"" - ] - } - ] -}"; - - SetupHttpRequest(handlerMock, request, response); - - var result = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - result.Should().NotBeNull(); - - result.Exclusions.Should().BeEquivalentTo("**/value1", "**/value2", "**/some/value/3"); - result.GlobalExclusions.Should().BeEquivalentTo("**/some/value/4"); - result.Inclusions.Should().BeEquivalentTo("**/111"); - } - - private static GetExclusionsRequest CreateTestSubject(string projectKey) - { - var testSubject = new GetExclusionsRequest { Logger = new TestLogger(), ProjectKey = projectKey }; - - return testSubject; - } - } -} diff --git a/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesRequestTests.cs b/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesRequestTests.cs deleted file mode 100644 index ddfb5ad8d4..0000000000 --- a/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesRequestTests.cs +++ /dev/null @@ -1,466 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Net.Http; -using Moq; -using SonarQube.Client.Api.V7_20; -using SonarQube.Client.Tests.Infra; -using static SonarQube.Client.Tests.Infra.MocksHelper; - -namespace SonarQube.Client.Tests.Requests.Api.V7_20 -{ - [TestClass] - public class GetIssuesRequestTests - { - [TestMethod] - public async Task InvokeAsync_FilePathNormalized() - { - const string projectKey = "myproject"; - const string statusesToRequest = "some status"; - const string expectedEscapedStatusesInRequest = "some+status"; - - var testSubject = CreateTestSubject(projectKey, statusesToRequest); - - var handlerMock = new Mock(MockBehavior.Strict); - var httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri(ValidBaseAddress) }; - - // the contents of the json below were left untouched during the cleanup of the taint related methods & requests of SonarQube.Client - // because they still represent a valid issue format (with flows and secondary locations), - // while the fact that the contents have mentions of taint in them is irrelevant for this test - var request = $"api/issues/search?projects={projectKey}&statuses={expectedEscapedStatusesInRequest}&p=1&ps=500"; - const string response = @" -{ - ""total"": 1, - ""p"": 1, - ""ps"": 100, - ""paging"": { - ""pageIndex"": 1, - ""pageSize"": 100, - ""total"": 1 - }, - ""effortTotal"": 30, - ""debtTotal"": 30, - ""issues"": [ - { - ""key"": ""AXNZHuA7uQ67pPQjI7e7"", - ""rule"": ""roslyn.sonaranalyzer.security.cs:S5146"", - ""severity"": ""BLOCKER"", - ""component"": ""myprojectkey:projectroot/Controllers/WeatherForecastController.cs"", - ""project"": ""myprojectkey"", - ""line"": 43, - ""hash"": ""d8684fca55d4dc80e444a993de15ba18"", - ""textRange"": { - ""startLine"": 43, - ""endLine"": 43, - ""startOffset"": 19, - ""endOffset"": 43 - }, - ""status"": ""OPEN"", - ""message"": ""Refactor this code to not perform redirects based on tainted, user-controlled data."", - ""effort"": ""30min"", - ""debt"": ""30min"", - ""assignee"": ""rita-g-sonarsource@github"", - ""author"": ""rita.gorokhod@sonarsource.com"", - ""tags"": [], - ""creationDate"": ""2020-07-16T21:31:25+0200"", - ""updateDate"": ""2020-07-16T21:34:05+0200"", - ""type"": ""VULNERABILITY"", - ""organization"": ""myorganization"", - ""fromHotspot"": false - } - ], - ""components"": [ - { - ""organization"": ""myorganization"", - ""key"": ""myprojectkey:projectroot/Controllers/WeatherForecastController.cs"", - ""uuid"": ""AXNZHtnVuQ67pPQjI7ey"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""WeatherForecastController.cs"", - ""longName"": ""projectroot/Controllers/WeatherForecastController.cs"", - ""path"": ""projectroot/Controllers/WeatherForecastController.cs"" - } - ], - ""organizations"": [ - { - ""key"": ""myorganization"", - ""name"": ""a user"" - } - ], - ""facets"": [] -} -"; - - SetupHttpRequest(handlerMock, request, response); - - var results = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - results.Should().ContainSingle(); - - var result = results[0]; - result.FilePath.Should().Be("projectroot\\Controllers\\WeatherForecastController.cs"); - } - - [TestMethod] - public async Task InvokeAsync_ResponseWithFlows_IsDeserializedCorrectly() - { - const string projectKey = "myproject"; - const string statusesToRequest = "some status"; - const string expectedEscapedStatusesInRequest = "some+status"; - - var testSubject = CreateTestSubject(projectKey, statusesToRequest); - - var handlerMock = new Mock(MockBehavior.Strict); - var httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri(ValidBaseAddress) }; - - // the contents of the json below were left untouched during the cleanup of the taint related methods & requests of SonarQube.Client - // because they still represent a valid issue format (with flows and secondary locations), - // while the fact that the contents have mentions of taint in them is irrelevant for this test - var request = $"api/issues/search?projects={projectKey}&statuses={expectedEscapedStatusesInRequest}&p=1&ps=500"; - const string response = @" -{ - ""total"": 1, - ""p"": 1, - ""ps"": 100, - ""paging"": { - ""pageIndex"": 1, - ""pageSize"": 100, - ""total"": 1 - }, - ""effortTotal"": 30, - ""debtTotal"": 30, - ""issues"": [ - { - ""key"": ""AXNZHuA7uQ67pPQjI7e7"", - ""rule"": ""roslyn.sonaranalyzer.security.cs:S5146"", - ""severity"": ""BLOCKER"", - ""component"": ""myprojectkey:projectroot/Controllers/WeatherForecastController.cs"", - ""project"": ""myprojectkey"", - ""line"": 43, - ""hash"": ""d8684fca55d4dc80e444a993de15ba18"", - ""textRange"": { - ""startLine"": 43, - ""endLine"": 43, - ""startOffset"": 19, - ""endOffset"": 43 - }, - ""flows"": [ - { - ""locations"": [ - { - ""component"": ""myprojectkey:projectroot/Controllers/WeatherForecastController.cs"", - ""textRange"": { - ""startLine"": 43, - ""endLine"": 43, - ""startOffset"": 19, - ""endOffset"": 43 - }, - ""msg"": ""sink: tainted value is used to perform a security-sensitive operation"" - }, - { - ""component"": ""myprojectkey:projectroot/Controllers/WeatherForecastController.cs"", - ""textRange"": { - ""startLine"": 41, - ""endLine"": 41, - ""startOffset"": 16, - ""endOffset"": 58 - }, - ""msg"": ""tainted value is propagated"" - }, - { - ""component"": ""myprojectkey:projectroot/Controllers/WeatherForecastController.cs"", - ""textRange"": { - ""startLine"": 41, - ""endLine"": 41, - ""startOffset"": 28, - ""endOffset"": 58 - }, - ""msg"": ""tainted value is propagated"" - } - ] - }, - { - ""locations"": [ - { - ""component"": ""myprojectkey:projectroot/Controllers/Helper.cs"", - ""textRange"": { - ""startLine"": 7, - ""endLine"": 7, - ""startOffset"": 12, - ""endOffset"": 29 - }, - ""msg"": ""tainted value is propagated"" - }, - { - ""component"": ""myprojectkey:projectroot/Controllers/Helper.cs"", - ""textRange"": { - ""startLine"": 5, - ""endLine"": 5, - ""startOffset"": 29, - ""endOffset"": 41 - }, - ""msg"": ""tainted value is propagated"" - } - ], - } - ], - ""status"": ""OPEN"", - ""message"": ""Refactor this code to not perform redirects based on tainted, user-controlled data."", - ""effort"": ""30min"", - ""debt"": ""30min"", - ""assignee"": ""rita-g-sonarsource@github"", - ""author"": ""rita.gorokhod@sonarsource.com"", - ""tags"": [], - ""creationDate"": ""2020-07-16T21:31:25+0200"", - ""updateDate"": ""2020-07-16T21:34:05+0200"", - ""type"": ""VULNERABILITY"", - ""organization"": ""myorganization"", - ""fromHotspot"": false - } - ], - ""components"": [ - { - ""organization"": ""myorganization"", - ""key"": ""myprojectkey:projectroot/Controllers/WeatherForecastController.cs"", - ""uuid"": ""AXNZHtnVuQ67pPQjI7ey"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""WeatherForecastController.cs"", - ""longName"": ""projectroot/Controllers/WeatherForecastController.cs"", - ""path"": ""projectroot/Controllers/WeatherForecastController.cs"" - }, - { - ""organization"": ""myorganization"", - ""key"": ""myprojectkey:projectroot/Controllers/Helper.cs"", - ""uuid"": ""AXNZHtnVuQ67pPQjI7ez"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""Helper.cs"", - ""longName"": ""projectroot/Controllers/Helper.cs"", - ""path"": ""projectroot/Controllers/Helper.cs"" - }, - { - ""organization"": ""myorganization"", - ""key"": ""myprojectkey"", - ""uuid"": ""AXJLrCxxxeWiK2BCzDif"", - ""enabled"": true, - ""qualifier"": ""TRK"", - ""name"": ""sanity-connected"", - ""longName"": ""sanity-connected"" - } - ], - ""organizations"": [ - { - ""key"": ""myorganization"", - ""name"": ""a user"" - } - ], - ""facets"": [] -} -"; - - SetupHttpRequest(handlerMock, request, response); - - var results = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - results.Should().ContainSingle(); - - var result = results[0]; - - result.RuleId.Should().Be("roslyn.sonaranalyzer.security.cs:S5146"); - result.Flows.Count().Should().Be(2); - - result.Flows[0].Locations.Count().Should().Be(3); - result.Flows[1].Locations.Count().Should().Be(2); - - var firstFlowFirstLocation = results[0].Flows[0].Locations[0]; - firstFlowFirstLocation.ModuleKey.Should().Be("myprojectkey:projectroot/Controllers/WeatherForecastController.cs"); - firstFlowFirstLocation.FilePath.Should().Be("projectroot\\Controllers\\WeatherForecastController.cs"); - firstFlowFirstLocation.Message.Should().Be("sink: tainted value is used to perform a security-sensitive operation"); - firstFlowFirstLocation.TextRange.StartLine.Should().Be(43); - firstFlowFirstLocation.TextRange.EndLine.Should().Be(43); - firstFlowFirstLocation.TextRange.StartOffset.Should().Be(19); - firstFlowFirstLocation.TextRange.EndOffset.Should().Be(43); - - var seconFlowSecondLocation = results[0].Flows[1].Locations[1]; - seconFlowSecondLocation.ModuleKey.Should().Be("myprojectkey:projectroot/Controllers/Helper.cs"); - seconFlowSecondLocation.FilePath.Should().Be("projectroot\\Controllers\\Helper.cs"); - seconFlowSecondLocation.Message.Should().Be("tainted value is propagated"); - seconFlowSecondLocation.TextRange.StartLine.Should().Be(5); - seconFlowSecondLocation.TextRange.EndLine.Should().Be(5); - seconFlowSecondLocation.TextRange.StartOffset.Should().Be(29); - seconFlowSecondLocation.TextRange.EndOffset.Should().Be(41); - } - - [TestMethod] - [DataRow("")] - [DataRow(null)] - public async Task InvokeAsync_BranchIsNotSpecified_BranchIsNotIncludedInQueryString(string emptyBranch) - { - var testSubject = CreateTestSubject("any", "any", emptyBranch); - - var handlerMock = new Mock(); - var httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri(ValidBaseAddress) }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - // Branch is null/empty => should not be passed - var actualQueryString = GetSingleActualQueryString(handlerMock); - actualQueryString.Contains("branch").Should().BeFalse(); - } - - [TestMethod] - public async Task InvokeAsync_BranchIsSpecified_BranchIsIncludedInQueryString() - { - const string requestedBranch = "mybranch"; - - var testSubject = CreateTestSubject("any", "any", requestedBranch); - - var handlerMock = new Mock(MockBehavior.Strict); - var httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri(ValidBaseAddress) }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - // Branch is not null/empty => should be passed - var actualQueryString = GetSingleActualQueryString(handlerMock); - actualQueryString.Contains($"&branch={requestedBranch}&").Should().BeTrue(); - } - - [TestMethod] - public async Task InvokeAsync_IssueKeysAreNotSpecified_IssueKeysAreNotIncludedInQueryString() - { - var testSubject = CreateTestSubject("any", "any", issueKeys: null); - - var handlerMock = new Mock(); - var httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri(ValidBaseAddress) }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - var actualQueryString = GetSingleActualQueryString(handlerMock); - actualQueryString.Contains("issues").Should().BeFalse(); - } - - [TestMethod] - public async Task InvokeAsync_IssueKeysAreSpecified_IssueKeysAreIncludedInQueryString() - { - var issueKeys = new[] { "issue1", "issue2" }; - var testSubject = CreateTestSubject("any", "any", issueKeys: issueKeys); - - var handlerMock = new Mock(); - var httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri(ValidBaseAddress) }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - var actualQueryString = GetSingleActualQueryString(handlerMock); - actualQueryString.Contains("issues=issue1%2Cissue2").Should().BeTrue(); - } - - [TestMethod] - public async Task InvokeAsync_RuleIdNotSpecified_RulesAreNotIncludedInQueryString() - { - var testSubject = CreateTestSubject("any", "any", ruleId: null); - - var handlerMock = new Mock(); - var httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri(ValidBaseAddress) }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - var actualQueryString = GetSingleActualQueryString(handlerMock); - actualQueryString.Contains("rules").Should().BeFalse(); - } - - [TestMethod] - public async Task InvokeAsync_RuleIdSpecified_RulesAreIncludedInQueryString() - { - var testSubject = CreateTestSubject("any", "any", ruleId: "rule1"); - - var handlerMock = new Mock(); - var httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri(ValidBaseAddress) }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - var actualQueryString = GetSingleActualQueryString(handlerMock); - actualQueryString.Contains("rules=rule1").Should().BeTrue(); - } - - [TestMethod] - public async Task InvokeAsync_ComponentKeyNotSpecified_ComponentsAreNotIncludedInQueryString() - { - var testSubject = CreateTestSubject("any", "any", componentKey: null); - - var handlerMock = new Mock(); - var httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri(ValidBaseAddress) }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - var actualQueryString = GetSingleActualQueryString(handlerMock); - actualQueryString.Should().NotContain("component"); - } - - [TestMethod] - public async Task InvokeAsync_ComponentKeySpecified_ComponentsAreNotIncludedInQueryString() - { - var testSubject = CreateTestSubject("any", "any", componentKey: "project1"); - - var handlerMock = new Mock(); - var httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri(ValidBaseAddress) }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - var actualQueryString = GetSingleActualQueryString(handlerMock); - actualQueryString.Should().NotContain("component"); - } - - private static GetIssuesRequest CreateTestSubject( - string projectKey, - string statusesToRequest, - string branch = null, - string[] issueKeys = null, - string ruleId = null, - string componentKey = null) - { - var testSubject = new GetIssuesRequest - { - Logger = new TestLogger(), - ProjectKey = projectKey, - Statuses = statusesToRequest, - Branch = branch, - IssueKeys = issueKeys, - RuleId = ruleId, - }; - - return testSubject; - } - - private static string GetSingleActualQueryString(Mock handlerMock) - { - handlerMock.Invocations.Count.Should().Be(1); - var requestMessage = (HttpRequestMessage)handlerMock.Invocations[0].Arguments[0]; - return requestMessage.RequestUri.Query; - } - } -} diff --git a/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesRequestWrapperTests.cs b/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesRequestWrapperTests.cs deleted file mode 100644 index 9c569b1954..0000000000 --- a/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesRequestWrapperTests.cs +++ /dev/null @@ -1,152 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Net.Http; -using Moq; -using SonarQube.Client.Api; -using SonarQube.Client.Api.V7_20; -using SonarQube.Client.Tests.Infra; -using static SonarQube.Client.Tests.Infra.MocksHelper; - -namespace SonarQube.Client.Tests.Requests.Api.V7_20; - -[TestClass] -public class GetIssuesRequestWrapperTests -{ - private const string ComponentPropertyNameSonarQube = "components"; - private const string ComponentPropertyNameSonarCloud = "componentKeys"; - - [DataTestMethod] - [DataRow(ComponentPropertyNameSonarQube, DisplayName = "SonarQube")] - [DataRow(ComponentPropertyNameSonarCloud, DisplayName = "SonarCloud")] - public async Task InvokeAsync_NoIssueKeys_ExpectedPropertiesArePassedInMultipleRequests(string componentPropertyName) - { - var testSubject = CreateTestSubject(componentPropertyName, "aaaProject", "xStatus", "yBranch", null, "rule1", "component1"); - - var handlerMock = new Mock(MockBehavior.Strict); - var httpClient = new HttpClient(handlerMock.Object) - { - BaseAddress = new Uri(ValidBaseAddress) - }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - // The wrapper is expected to make three calls, for code smells, bugs, then vulnerabilities - handlerMock.Invocations.Count.Should().Be(2); - CheckExpectedQueryStringsParameters(componentPropertyName, handlerMock, 0, expectedTypes: "CODE_SMELL"); - CheckExpectedQueryStringsParameters(componentPropertyName, handlerMock, 1, expectedTypes: "BUG"); - } - - [DataTestMethod] - [DataRow(ComponentPropertyNameSonarQube, DisplayName = "SonarQube")] - [DataRow(ComponentPropertyNameSonarCloud, DisplayName = "SonarCloud")] - public async Task InvokeAsync_HasIssueKeys_ExpectedPropertiesArePassedInASingleRequest(string componentPropertyName) - { - var issueKeys = new[] { "issue1", "issue2" }; - var testSubject = CreateTestSubject(componentPropertyName,"aaaProject", "xStatus", "yBranch", issueKeys, "rule1", "component1"); - - var handlerMock = new Mock(MockBehavior.Strict); - var httpClient = new HttpClient(handlerMock.Object) - { - BaseAddress = new Uri(ValidBaseAddress) - }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - // The wrapper is expected to make one call with the given issueKeys - handlerMock.Invocations.Count.Should().Be(1); - - CheckExpectedQueryStringsParameters(componentPropertyName, handlerMock, 0, expectedKeys: issueKeys); - } - - private static IGetIssuesRequest CreateTestSubject(string componentPropertyName, string projectKey, string statusesToRequest, string branch, string[] issueKeys, string ruleId, string componentKey) - { - return componentPropertyName switch - { - ComponentPropertyNameSonarQube => new GetIssuesRequestWrapper - { - Logger = new TestLogger(), - ProjectKey = projectKey, - Statuses = statusesToRequest, - Branch = branch, - IssueKeys = issueKeys, - RuleId = ruleId, - ComponentKey = componentKey - }, - ComponentPropertyNameSonarCloud => new GetIssuesRequestWrapper - { - Logger = new TestLogger(), - ProjectKey = projectKey, - Statuses = statusesToRequest, - Branch = branch, - IssueKeys = issueKeys, - RuleId = ruleId, - ComponentKey = componentKey - }, - _ => throw new ArgumentOutOfRangeException() - }; - } - - private static void CheckExpectedQueryStringsParameters(string componentKeyName, - Mock handlerMock, - int invocationIndex, - string expectedTypes = null, - string[] expectedKeys = null) - { - var actualQueryString = GetActualQueryStringForInvocation(handlerMock, invocationIndex); - - Console.WriteLine($"Invocation [{invocationIndex}]: {actualQueryString}"); - actualQueryString.Contains($"?{componentKeyName}=component1").Should().BeTrue(); - actualQueryString.Contains("&projects=aaaProject").Should().BeTrue(); - actualQueryString.Contains("&statuses=xStatus").Should().BeTrue(); - actualQueryString.Contains("&branch=yBranch").Should().BeTrue(); - actualQueryString.Contains("&rules=rule1").Should().BeTrue(); - - if (expectedTypes != null) - { - actualQueryString.Contains($"&types={expectedTypes}").Should().BeTrue(); - } - else - { - actualQueryString.Contains("types").Should().BeFalse(); - } - - if (expectedKeys != null) - { - var keys = string.Join("%2C", expectedKeys); - actualQueryString.Contains($"&issues={keys}").Should().BeTrue(); - } - else - { - actualQueryString.Contains("issues").Should().BeFalse(); - } - - } - - private static string GetActualQueryStringForInvocation(Mock handlerMock, int invocationIndex) - { - var requestMessage = (HttpRequestMessage)handlerMock.Invocations[invocationIndex].Arguments[0]; - return requestMessage.RequestUri.Query; - } -} diff --git a/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesWithComponentSonarCloudRequestTests.cs b/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesWithComponentSonarCloudRequestTests.cs deleted file mode 100644 index 28b62b3354..0000000000 --- a/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesWithComponentSonarCloudRequestTests.cs +++ /dev/null @@ -1,92 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using SonarQube.Client.Api.V7_20; -using SonarQube.Client.Tests.Infra; -using static SonarQube.Client.Tests.Infra.MocksHelper; - -namespace SonarQube.Client.Tests.Requests.Api.V7_20; - -[TestClass] -public class GetIssuesWithComponentSonarCloudRequestTests -{ - [TestMethod] - public async Task InvokeAsync_ComponentKeyNotSpecified_ComponentsAreNotIncludedInQueryString() - { - var testSubject = CreateTestSubject("any", "any", componentKey: null); - - var handlerMock = new Mock(); - var httpClient = new HttpClient(handlerMock.Object) - { - BaseAddress = new Uri(ValidBaseAddress) - }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - var actualQueryString = GetSingleActualQueryString(handlerMock); - actualQueryString.Should().NotContain("component"); - } - - [TestMethod] - public async Task InvokeAsync_ComponentKeySpecified_ComponentsAreIncludedInQueryString() - { - var testSubject = CreateTestSubject("any", "any", componentKey: "project1"); - - var handlerMock = new Mock(); - var httpClient = new HttpClient(handlerMock.Object) - { - BaseAddress = new Uri(ValidBaseAddress) - }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - var actualQueryString = GetSingleActualQueryString(handlerMock); - actualQueryString.Should().Contain("componentKeys=project1"); - } - - private static GetIssuesWithComponentSonarCloudRequest CreateTestSubject(string projectKey, string statusesToRequest, string componentKey = null) - { - var testSubject = new GetIssuesWithComponentSonarCloudRequest - { - Logger = new TestLogger(), - ProjectKey = projectKey, - Statuses = statusesToRequest, - ComponentKey = componentKey - }; - - return testSubject; - } - - private static string GetSingleActualQueryString(Mock handlerMock) - { - handlerMock.Invocations.Count.Should().Be(1); - var requestMessage = (HttpRequestMessage)handlerMock.Invocations[0].Arguments[0]; - return requestMessage.RequestUri.Query; - } -} diff --git a/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesWithComponentSonarQubeRequestTests.cs b/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesWithComponentSonarQubeRequestTests.cs deleted file mode 100644 index 8e4d636bbd..0000000000 --- a/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesWithComponentSonarQubeRequestTests.cs +++ /dev/null @@ -1,92 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using SonarQube.Client.Api.V7_20; -using SonarQube.Client.Tests.Infra; -using static SonarQube.Client.Tests.Infra.MocksHelper; - -namespace SonarQube.Client.Tests.Requests.Api.V7_20; - -[TestClass] -public class GetIssuesWithComponentSonarQubeRequestTests -{ - [TestMethod] - public async Task InvokeAsync_ComponentKeyNotSpecified_ComponentsAreNotIncludedInQueryString() - { - var testSubject = CreateTestSubject("any", "any", componentKey: null); - - var handlerMock = new Mock(); - var httpClient = new HttpClient(handlerMock.Object) - { - BaseAddress = new Uri(ValidBaseAddress) - }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - var actualQueryString = GetSingleActualQueryString(handlerMock); - actualQueryString.Should().NotContain("component"); - } - - [TestMethod] - public async Task InvokeAsync_ComponentKeySpecified_ComponentsAreIncludedInQueryString() - { - var testSubject = CreateTestSubject("any", "any", componentKey: "project1"); - - var handlerMock = new Mock(); - var httpClient = new HttpClient(handlerMock.Object) - { - BaseAddress = new Uri(ValidBaseAddress) - }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - var actualQueryString = GetSingleActualQueryString(handlerMock); - actualQueryString.Should().Contain("components=project1"); - } - - private static GetIssuesWithComponentSonarQubeRequest CreateTestSubject(string projectKey, string statusesToRequest, string componentKey = null) - { - var testSubject = new GetIssuesWithComponentSonarQubeRequest - { - Logger = new TestLogger(), - ProjectKey = projectKey, - Statuses = statusesToRequest, - ComponentKey = componentKey - }; - - return testSubject; - } - - private static string GetSingleActualQueryString(Mock handlerMock) - { - handlerMock.Invocations.Count.Should().Be(1); - var requestMessage = (HttpRequestMessage)handlerMock.Invocations[0].Arguments[0]; - return requestMessage.RequestUri.Query; - } -} diff --git a/src/SonarQube.Client.Tests/Requests/Api/V9_4/GetSonarLintEventStreamTests.cs b/src/SonarQube.Client.Tests/Requests/Api/V9_4/GetSonarLintEventStreamTests.cs deleted file mode 100644 index 75571c303d..0000000000 --- a/src/SonarQube.Client.Tests/Requests/Api/V9_4/GetSonarLintEventStreamTests.cs +++ /dev/null @@ -1,58 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.IO; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using Moq; -using SonarQube.Client.Api.V9_4; -using SonarQube.Client.Logging; -using SonarQube.Client.Tests.Infra; - -namespace SonarQube.Client.Tests.Requests.Api.V9_4; - -[TestClass] -public class GetSonarLintEventStreamTests -{ - [TestMethod] - public async Task InvokeAsync_ReturnsCorrectStream() - { - using var testedStream = new MemoryStream(Encoding.UTF8.GetBytes("hello this is a test")); - var messageHandler = new Mock(); - using var httpClient = new HttpClient(messageHandler.Object) { BaseAddress = new Uri("http://localhost") }; - - MocksHelper.SetupHttpRequest( - messageHandler, - requestRelativePath: "api/push/sonarlint_events?languages=cs%2Cvbnet&projectKeys=someproj", - responseMessage: new HttpResponseMessage { Content = new StreamContent(testedStream) }, - headers: MediaTypeHeaderValue.Parse("text/event-stream")); - - var testSubject = new GetSonarLintEventStream { ProjectKey = "someproj", Logger = Mock.Of() }; - - using var response = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - response.Should().NotBeNull(); - messageHandler.VerifyAll(); - - using var reader = new StreamReader(response, Encoding.UTF8); - var responseString = await reader.ReadToEndAsync(); - responseString.Should().Be("hello this is a test"); - } -} diff --git a/src/SonarQube.Client.Tests/Requests/DefaultConfiguration_Configure_Tests.cs b/src/SonarQube.Client.Tests/Requests/DefaultConfiguration_Configure_Tests.cs index 1a405e82c7..584708e6a4 100644 --- a/src/SonarQube.Client.Tests/Requests/DefaultConfiguration_Configure_Tests.cs +++ b/src/SonarQube.Client.Tests/Requests/DefaultConfiguration_Configure_Tests.cs @@ -35,19 +35,8 @@ public void ConfigureSonarQube_Writes_Debug_Messages() var expected = new[] { "Registered SonarQube.Client.Api.V2_10.GetVersionRequest for 2.1", - "Registered SonarQube.Client.Api.V2_60.GetPropertiesRequest for 2.6", "Registered SonarQube.Client.Api.V3_30.ValidateCredentialsRequest for 3.3", - "Registered SonarQube.Client.Api.V5_20.GetQualityProfilesRequest for 5.2", - "Registered SonarQube.Client.Api.V5_50.GetRulesRequest for 5.5", - "Registered SonarQube.Client.Api.V6_30.GetPropertiesRequest for 6.3", - "Registered SonarQube.Client.Api.V6_50.GetQualityProfilesRequest for 6.5", "Registered SonarQube.Client.Api.V6_60.GetNotificationsRequest for 6.6", - "Registered SonarQube.Client.Api.V6_60.GetProjectBranchesRequest for 6.6", - "Registered SonarQube.Client.Api.V7_20.GetIssuesRequestWrapper`1[SonarQube.Client.Api.V7_20.GetIssuesWithComponentSonarQubeRequest] for 7.2", - "Registered SonarQube.Client.Api.V7_20.GetExclusionsRequest for 7.2", - "Registered SonarQube.Client.Api.V9_4.GetSonarLintEventStream for 9.4", - "Registered SonarQube.Client.Api.V10_2.GetRulesWithCCTRequest for 10.2", - "Registered SonarQube.Client.Api.V9_9.SearchFilesByNameRequest for 9.9", }; DefaultConfiguration.ConfigureSonarQube(new RequestFactory(logger)); @@ -67,14 +56,7 @@ public void ConfigureSonarCloud_Writes_Debug_Messages() { "Registered SonarQube.Client.Api.V2_10.GetVersionRequest", "Registered SonarQube.Client.Api.V3_30.ValidateCredentialsRequest", - "Registered SonarQube.Client.Api.V10_2.GetRulesWithCCTRequest", - "Registered SonarQube.Client.Api.V6_30.GetPropertiesRequest", - "Registered SonarQube.Client.Api.V6_50.GetQualityProfilesRequest", "Registered SonarQube.Client.Api.V6_60.GetNotificationsRequest", - "Registered SonarQube.Client.Api.V6_60.GetProjectBranchesRequest", - "Registered SonarQube.Client.Api.V7_20.GetIssuesRequestWrapper`1[SonarQube.Client.Api.V7_20.GetIssuesWithComponentSonarCloudRequest]", - "Registered SonarQube.Client.Api.V7_20.GetExclusionsRequest", - "Registered SonarQube.Client.Api.V9_9.SearchFilesByNameRequest" }; DefaultConfiguration.ConfigureSonarCloud(new UnversionedRequestFactory(logger)); @@ -91,17 +73,9 @@ public void ConfigureSonarQube_CheckAllRequestsImplemented() var testSubject = DefaultConfiguration.ConfigureSonarQube(new RequestFactory(new TestLogger())); var serverInfo = new ServerInfo(null /* latest */, ServerType.SonarQube); - testSubject.Create(serverInfo).Should().NotBeNull(); testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); testSubject.Create(serverInfo).Should().NotBeNull(); testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); } [TestMethod] @@ -110,28 +84,9 @@ public void ConfigureSonarCloud_CheckAllRequestsImplemented() var testSubject = DefaultConfiguration.ConfigureSonarCloud(new UnversionedRequestFactory(new TestLogger())); var serverInfo = new ServerInfo(null /* latest */, ServerType.SonarQube); - testSubject.Create(serverInfo).Should().NotBeNull(); testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); testSubject.Create(serverInfo).Should().NotBeNull(); testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - } - - [TestMethod] - [Description("The following APIs are not implemented on SC (yet). Verify that they are not registered in the factory.")] - public void ConfigureSonarCloud_CheckUnsupportedRequestsAreNotImplemented() - { - var testSubject = DefaultConfiguration.ConfigureSonarCloud(new UnversionedRequestFactory(new TestLogger())); - var serverInfo = new ServerInfo(null /* latest */, ServerType.SonarQube); - - Action act = () => testSubject.Create(serverInfo); - act.Should().Throw().And.Message.Should() - .Be("Could not find factory for 'IGetSonarLintEventStream'."); } private static void DumpDebugMessages(TestLogger logger) diff --git a/src/SonarQube.Client.Tests/SonarQube.Client.Tests.csproj b/src/SonarQube.Client.Tests/SonarQube.Client.Tests.csproj index ff3522c5fd..69e42f4038 100644 --- a/src/SonarQube.Client.Tests/SonarQube.Client.Tests.csproj +++ b/src/SonarQube.Client.Tests/SonarQube.Client.Tests.csproj @@ -12,10 +12,6 @@ true - - - - @@ -25,9 +21,7 @@ - - Always - + \ No newline at end of file diff --git a/src/SonarQube.Client.Tests/SonarQubeService_CreateServerSentEventsSession.cs b/src/SonarQube.Client.Tests/SonarQubeService_CreateServerSentEventsSession.cs deleted file mode 100644 index 862af07d1a..0000000000 --- a/src/SonarQube.Client.Tests/SonarQubeService_CreateServerSentEventsSession.cs +++ /dev/null @@ -1,84 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.IO; -using System.Net.Http; -using System.Net.Http.Headers; -using Moq; -using SonarQube.Client.Models.ServerSentEvents; - -namespace SonarQube.Client.Tests; - -[TestClass] -public class SonarQubeService_CreateSSEStreamReader : SonarQubeService_TestBase -{ - [TestMethod] - public async Task CreateSSEStreamReader_SupportedServerVersion_CreatesSession() - { - var expectedCreatedSSEStream = Mock.Of(); - - sseStreamFactory - .Setup(x => x.Create(It.IsAny(), CancellationToken.None)) - .Returns(expectedCreatedSSEStream); - - await ConnectToSonarQube("9.4.0.0"); - - SetupRequest("api/push/sonarlint_events?languages=cs%2Cvbnet&projectKeys=myProject", - new HttpResponseMessage - { - Content = new StreamContent(Stream.Null) - }, - MediaTypeHeaderValue.Parse("text/event-stream")); - - var result = await service.CreateSSEStreamReader("myProject", CancellationToken.None); - - result.Should().NotBeNull(); - result.Should().Be(expectedCreatedSSEStream); - } - - [TestMethod] - public async Task CreateSSEStreamReader_UnsupportedServerVersion_InvalidOperationException() - { - await ConnectToSonarQube("3.3.0.0"); - - Func> func = async () => await service.CreateSSEStreamReader("myProject", CancellationToken.None); - - const string expectedErrorMessage = - "Could not find compatible implementation of 'IGetSonarLintEventStream' for SonarQube 3.3.0.0."; - - func.Should().ThrowExactly().WithMessage(expectedErrorMessage); - - logger.ErrorMessages.Should().Contain(expectedErrorMessage); - } - - [TestMethod] - public void CreateSSEStreamReader_NotConnected_InvalidOperationException() - { - // No calls to Connect - // No need to setup request, the operation should fail - - Func> func = async () => await service.CreateSSEStreamReader("myProject", CancellationToken.None); - - func.Should().ThrowExactly() - .WithMessage("This operation expects the service to be connected."); - - logger.ErrorMessages.Should().Contain("The service is expected to be connected."); - } -} diff --git a/src/SonarQube.Client.Tests/SonarQubeService_GetAllPropertiesAsync.cs b/src/SonarQube.Client.Tests/SonarQubeService_GetAllPropertiesAsync.cs deleted file mode 100644 index 74da9bc20d..0000000000 --- a/src/SonarQube.Client.Tests/SonarQubeService_GetAllPropertiesAsync.cs +++ /dev/null @@ -1,219 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Net; -using System.Net.Http; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Tests -{ - [TestClass] - public class SonarQubeService_GetAllPropertiesAsync : SonarQubeService_TestBase - { - [TestMethod] - public async Task GetProperties_Old_ExampleFromSonarQube() - { - await ConnectToSonarQube(); - - SetupRequest("api/properties?resource=my-project", @"[ - { - ""key"": ""sonar.test.jira"", - ""value"": ""abc"" - }, - { - ""key"": ""sonar.autogenerated"", - ""value"": ""val1,val2,val3"", - ""values"": [ - ""val1"", - ""val2"", - ""val3"" - ] - }, - { - ""key"": ""sonar.demo"", - ""value"": ""1,2"", - ""values"": [ - ""1"", - ""2"" - ] - }, - { - ""key"": ""sonar.demo.1.text"", - ""value"": ""foo"" - }, - { - ""key"": ""sonar.demo.1.boolean"", - ""value"": ""true"" - }, - { - ""key"": ""sonar.demo.2.text"", - ""value"": ""bar"" - }, - { - ""key"": ""sonar.demo.2.boolean"", - ""value"": ""false"" - } -]"); - - var result = await service.GetAllPropertiesAsync("my-project", CancellationToken.None); - - httpClientHandler.VerifyAll(); - - result.Should().HaveCount(7); - result.Select(x => x.Key).Should().BeEquivalentTo( - new[] { "sonar.test.jira", "sonar.autogenerated", "sonar.demo", "sonar.demo.1.text", "sonar.demo.1.boolean", "sonar.demo.2.text", "sonar.demo.2.boolean" }); - result.Select(x => x.Value).Should().BeEquivalentTo( - new[] { "abc", "val1,val2,val3", "1,2", "foo", "true", "bar", "false" }); - } - - [TestMethod] - public async Task GetProperties_Old_NotFound() - { - await ConnectToSonarQube(); - - SetupRequest("api/properties", "", HttpStatusCode.NotFound); - - Func>> func = async () => - await service.GetAllPropertiesAsync(null, CancellationToken.None); - - func.Should().ThrowExactly().And - .Message.Should().Be("Response status code does not indicate success: 404 (Not Found)."); - - httpClientHandler.VerifyAll(); - } - - [TestMethod] - public async Task GetProperties_ExampleFromSonarQube() - { - await ConnectToSonarQube("6.3.0.0"); - - SetupRequest("api/settings/values?component=my-project", @"{ - ""settings"": [ - { - ""key"": ""sonar.test.jira"", - ""value"": ""abc"", - ""inherited"": true - }, - { - ""key"": ""sonar.autogenerated"", - ""values"": [ - ""val1"", - ""val2"", - ""val3"" - ], - ""inherited"": false - }, - { - ""key"": ""sonar.demo"", - ""fieldValues"": [ - { - ""boolean"": ""true"", - ""text"": ""foo"" - }, - { - ""boolean"": ""false"", - ""text"": ""bar"" - } - ], - ""inherited"": false - } - ] -}"); - - var result = await service.GetAllPropertiesAsync("my-project", CancellationToken.None); - - httpClientHandler.VerifyAll(); - - result.Should().HaveCount(6); - result.Select(x => x.Key).Should().ContainInOrder( - new[] { "sonar.test.jira", "sonar.autogenerated", "sonar.demo.1.boolean", "sonar.demo.1.text", "sonar.demo.2.boolean", "sonar.demo.2.text", }); - - result.Select(x => x.Value).Should().ContainInOrder( - new[] { "abc", "val1,val2,val3", "true", "foo", "false", "bar" }); - } - - [TestMethod] - public async Task GetProperties_ExampleFromSonarQube_Project_NotFound() - { - await ConnectToSonarQube("6.3.0.0"); - - SetupRequest("api/settings/values?component=my-project", "", HttpStatusCode.NotFound); - - SetupRequest("api/settings/values", @"{ - ""settings"": [ - { - ""key"": ""sonar.test.jira"", - ""value"": ""abc"", - ""inherited"": true - }, - { - ""key"": ""sonar.autogenerated"", - ""values"": [ - ""val1"", - ""val2"", - ""val3"" - ], - ""inherited"": false - }, - { - ""key"": ""sonar.demo"", - ""fieldValues"": [ - { - ""boolean"": ""true"", - ""text"": ""foo"" - }, - { - ""boolean"": ""false"", - ""text"": ""bar"" - } - ], - ""inherited"": false - } - ] -}"); - - var result = await service.GetAllPropertiesAsync("my-project", CancellationToken.None); - - httpClientHandler.VerifyAll(); - - result.Should().HaveCount(6); - result.Select(x => x.Key).Should().ContainInOrder( - new[] { "sonar.test.jira", "sonar.autogenerated", "sonar.demo.1.boolean", "sonar.demo.1.text", "sonar.demo.2.boolean", "sonar.demo.2.text", }); - - result.Select(x => x.Value).Should().ContainInOrder( - new[] { "abc", "val1,val2,val3", "true", "foo", "false", "bar" }); - } - - [TestMethod] - public void GetProperties_NotConnected() - { - // No calls to Connect - // No need to setup request, the operation should fail - - Func>> func = async () => - await service.GetAllPropertiesAsync(null, CancellationToken.None); - - func.Should().ThrowExactly().And - .Message.Should().Be("This operation expects the service to be connected."); - - logger.ErrorMessages.Should().Contain("The service is expected to be connected."); - } - } -} diff --git a/src/SonarQube.Client.Tests/SonarQubeService_GetAllQualityProfilesAsync.cs b/src/SonarQube.Client.Tests/SonarQubeService_GetAllQualityProfilesAsync.cs deleted file mode 100644 index dd008c3f5b..0000000000 --- a/src/SonarQube.Client.Tests/SonarQubeService_GetAllQualityProfilesAsync.cs +++ /dev/null @@ -1,324 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Net; -using System.Net.Http; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Tests; - -[TestClass] -public class SonarQubeService_GetAllQualityProfilesAsync : SonarQubeService_TestBase -{ - [TestMethod] - public async Task GetAllQualityProfilesAsync_Old_ExampleFromSonarQube() - { - await ConnectToSonarQube(); - - SetupRequest("api/qualityprofiles/search?projectKey=my_project&organization=my_organization", @"{ - ""profiles"": [ - { - ""key"": ""AU-TpxcA-iU5OvuD2FL3"", - ""name"": ""Sonar way"", - ""language"": ""cs"", - ""languageName"": ""C#"", - ""isInherited"": false, - ""isBuiltIn"": true, - ""activeRuleCount"": 3, - ""activeDeprecatedRuleCount"": 0, - ""isDefault"": true, - ""rulesUpdatedAt"": ""2016-12-22T19:10:03+0100"", - ""lastUsed"": ""2016-12-01T19:10:03+0100"", - ""actions"": { - ""edit"": false, - ""setAsDefault"": false, - ""copy"": false - } - }, - { - ""key"": ""AU-TpxcA-iU5OvuD2FL1"", - ""name"": ""My BU Profile"", - ""language"": ""java"", - ""languageName"": ""Java"", - ""isInherited"": true, - ""isBuiltIn"": false, - ""parentKey"": ""iU5OvuD2FLz"", - ""parentName"": ""My Company Profile"", - ""activeRuleCount"": 15, - ""activeDeprecatedRuleCount"": 5, - ""isDefault"": false, - ""projectCount"": 7, - ""rulesUpdatedAt"": ""2016-12-20T19:10:03+0100"", - ""lastUsed"": ""2016-12-21T16:10:03+0100"", - ""userUpdatedAt"": ""2016-06-28T21:57:01+0200"", - ""actions"": { - ""edit"": true, - ""setAsDefault"": false, - ""copy"": false - } - }, - { - ""key"": ""AU-TpxcB-iU5OvuD2FL7"", - ""name"": ""Sonar way"", - ""language"": ""py"", - ""languageName"": ""Python"", - ""isInherited"": false, - ""isBuiltIn"": true, - ""activeRuleCount"": 2, - ""activeDeprecatedRuleCount"": 0, - ""isDefault"": true, - ""rulesUpdatedAt"": ""2014-12-22T19:10:03+0100"", - ""actions"": { - ""edit"": false, - ""setAsDefault"": false, - ""copy"": false - } - } - ], - ""actions"": { - ""create"": false - } -}"); - - var result = await service.GetAllQualityProfilesAsync("my_project", - "my_organization", - CancellationToken.None); - - httpClientHandler.VerifyAll(); - result.Select(x => x.Key).Should().BeEquivalentTo( - new[] { "AU-TpxcA-iU5OvuD2FL3", "AU-TpxcA-iU5OvuD2FL1", "AU-TpxcB-iU5OvuD2FL7" }); - result.Select(x => x.TimeStamp).Should().BeEquivalentTo(new[] { "2016-12-22T19:10:03+0100", "2016-12-20T19:10:03+0100", "2014-12-22T19:10:03+0100" } - .Select(DateTime.Parse)); - } - - [TestMethod] - public async Task GetAllQualityProfilesAsync_Old_ProjectNotAnalyzed() - { - await ConnectToSonarQube(); - - SetupRequest("api/qualityprofiles/search?projectKey=my_project&organization=my_organization", "", - HttpStatusCode.NotFound); - - SetupRequest("api/qualityprofiles/search?organization=my_organization&defaults=true", @"{ - ""profiles"": [ - { - ""key"": ""AU-TpxcA-iU5OvuD2FL3"", - ""name"": ""Sonar way"", - ""language"": ""cs"", - ""languageName"": ""C#"", - ""isInherited"": false, - ""isBuiltIn"": true, - ""activeRuleCount"": 3, - ""activeDeprecatedRuleCount"": 0, - ""isDefault"": true, - ""rulesUpdatedAt"": ""2016-12-22T19:10:03+0100"", - ""lastUsed"": ""2016-12-01T19:10:03+0100"", - ""actions"": { - ""edit"": false, - ""setAsDefault"": false, - ""copy"": false - } - } - ], - ""actions"": { - ""create"": false - } -}"); - - var result = (await service.GetAllQualityProfilesAsync("my_project", - "my_organization", - CancellationToken.None)) - .Single(); - - httpClientHandler.VerifyAll(); - - result.Should().NotBeNull(); - result.IsDefault.Should().BeTrue(); - result.Key.Should().Be("AU-TpxcA-iU5OvuD2FL3"); - result.Language.Should().Be("cs"); - result.Name.Should().Be("Sonar way"); - result.TimeStamp.Should().Be(DateTime.Parse("2016-12-22T19:10:03+0100")); - } - - [TestMethod] - public async Task GetAllQualityProfilesAsync_Old_Error() - { - await ConnectToSonarQube(); - - SetupRequest("api/qualityprofiles/search?projectKey=my_project&organization=my_organization", "", - HttpStatusCode.InternalServerError); - - Func>> func = async () => - await service.GetAllQualityProfilesAsync("my_project", "my_organization", CancellationToken.None); - - func.Should().ThrowExactly().And - .Message.Should().Be("Response status code does not indicate success: 500 (Internal Server Error)."); - - httpClientHandler.VerifyAll(); - } - - [TestMethod] - public async Task GetAllQualityProfiles_New_ExampleFromSonarQube() - { - await ConnectToSonarQube("6.5.0.0"); - - SetupRequest("api/qualityprofiles/search?project=my_project&organization=my_organization", @"{ - ""profiles"": [ - { - ""key"": ""AU-TpxcA-iU5OvuD2FL3"", - ""name"": ""Sonar way"", - ""language"": ""cs"", - ""languageName"": ""C#"", - ""isInherited"": false, - ""isBuiltIn"": true, - ""activeRuleCount"": 3, - ""activeDeprecatedRuleCount"": 0, - ""isDefault"": true, - ""rulesUpdatedAt"": ""2016-12-22T19:10:03+0100"", - ""lastUsed"": ""2016-12-01T19:10:03+0100"", - ""actions"": { - ""edit"": false, - ""setAsDefault"": false, - ""copy"": false - } - }, - { - ""key"": ""AU-TpxcA-iU5OvuD2FL1"", - ""name"": ""My BU Profile"", - ""language"": ""java"", - ""languageName"": ""Java"", - ""isInherited"": true, - ""isBuiltIn"": false, - ""parentKey"": ""iU5OvuD2FLz"", - ""parentName"": ""My Company Profile"", - ""activeRuleCount"": 15, - ""activeDeprecatedRuleCount"": 5, - ""isDefault"": false, - ""projectCount"": 7, - ""rulesUpdatedAt"": ""2016-12-20T19:10:03+0100"", - ""lastUsed"": ""2016-12-21T16:10:03+0100"", - ""userUpdatedAt"": ""2016-06-28T21:57:01+0200"", - ""actions"": { - ""edit"": true, - ""setAsDefault"": false, - ""copy"": false - } - }, - { - ""key"": ""AU-TpxcB-iU5OvuD2FL7"", - ""name"": ""Sonar way"", - ""language"": ""py"", - ""languageName"": ""Python"", - ""isInherited"": false, - ""isBuiltIn"": true, - ""activeRuleCount"": 2, - ""activeDeprecatedRuleCount"": 0, - ""isDefault"": true, - ""rulesUpdatedAt"": ""2014-12-22T19:10:03+0100"", - ""actions"": { - ""edit"": false, - ""setAsDefault"": false, - ""copy"": false - } - } - ], - ""actions"": { - ""create"": false - } -}"); - - var result = await service.GetAllQualityProfilesAsync("my_project", - "my_organization", - CancellationToken.None); - - httpClientHandler.VerifyAll(); - result.Select(x => x.Key).Should().BeEquivalentTo( - new[] { "AU-TpxcA-iU5OvuD2FL3", "AU-TpxcA-iU5OvuD2FL1", "AU-TpxcB-iU5OvuD2FL7" }); - result.Select(x => x.TimeStamp).Should().BeEquivalentTo( - new[] { "2016-12-22T19:10:03+0100", "2016-12-20T19:10:03+0100", "2014-12-22T19:10:03+0100" } - .Select(DateTime.Parse)); - - // Regression test for #1450 - shouldn't get a warning about max items retrieved - // https://github.com/SonarSource/sonarlint-visualstudio/issues/1450 - logger.WarningMessages.Count.Should().Be(0); - } - - [TestMethod] - public async Task GetAllQualityProfiles_New_ProjectNotAnalyzed() - { - await ConnectToSonarQube("6.5.0.0"); - - SetupRequest("api/qualityprofiles/search?project=my_project&organization=my_organization", "", - HttpStatusCode.NotFound); - - SetupRequest("api/qualityprofiles/search?organization=my_organization&defaults=true", @"{ - ""profiles"": [ - { - ""key"": ""AU-TpxcA-iU5OvuD2FL3"", - ""name"": ""Sonar way"", - ""language"": ""cs"", - ""languageName"": ""C#"", - ""isInherited"": false, - ""isBuiltIn"": true, - ""activeRuleCount"": 3, - ""activeDeprecatedRuleCount"": 0, - ""isDefault"": true, - ""rulesUpdatedAt"": ""2016-12-22T19:10:03+0100"", - ""lastUsed"": ""2016-12-01T19:10:03+0100"", - ""actions"": { - ""edit"": false, - ""setAsDefault"": false, - ""copy"": false - } - } - ], - ""actions"": { - ""create"": false - } -}"); - - var result = (await service.GetAllQualityProfilesAsync("my_project", - "my_organization", - CancellationToken.None)) - .Single(); - - result.IsDefault.Should().BeTrue(); - result.Key.Should().Be("AU-TpxcA-iU5OvuD2FL3"); - result.Language.Should().Be("cs"); - result.Name.Should().Be("Sonar way"); - result.TimeStamp.Should().Be(DateTime.Parse("2016-12-22T19:10:03+0100")); - } - - [TestMethod] - public void GetAllQualityProfiles_NotConnected() - { - // No calls to Connect - // No need to setup request, the operation should fail - - var func = new Func(async () => await service.GetAllQualityProfilesAsync("my_project", - "my_organization", - CancellationToken.None)); - - func.Should().ThrowExactly().And - .Message.Should().Be("This operation expects the service to be connected."); - - logger.ErrorMessages.Should().Contain("The service is expected to be connected."); - } -} diff --git a/src/SonarQube.Client.Tests/SonarQubeService_GetExclusionsRequest.cs b/src/SonarQube.Client.Tests/SonarQubeService_GetExclusionsRequest.cs deleted file mode 100644 index f601ebfe15..0000000000 --- a/src/SonarQube.Client.Tests/SonarQubeService_GetExclusionsRequest.cs +++ /dev/null @@ -1,82 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Net; -using System.Net.Http; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Tests -{ - [TestClass] - public class SonarQubeService_GetExclusionsRequest : SonarQubeService_TestBase - { - [TestMethod] - public void Get_NotConnected() - { - // No calls to Connect - // No need to setup request, the operation should fail - - Func> func = async () => - await service.GetServerExclusions("any", CancellationToken.None); - - func.Should().ThrowExactly().And - .Message.Should().Be("This operation expects the service to be connected."); - - logger.ErrorMessages.Should().Contain("The service is expected to be connected."); - } - - [TestMethod] - public async Task Get_NotFound() - { - await ConnectToSonarQube("7.2.0.0"); - - const string request = "api/settings/values?component=my_project&keys=sonar.exclusions%2Csonar.global.exclusions%2Csonar.inclusions"; - - SetupRequest(request, "", HttpStatusCode.NotFound); - - Func> func = async () => - await service.GetServerExclusions("my_project", CancellationToken.None); - - func.Should().ThrowExactly().And - .Message.Should().Be("Response status code does not indicate success: 404 (Not Found)."); - - httpClientHandler.VerifyAll(); - } - - [TestMethod] - public async Task Get_V7_2_ExampleFromSonarQube() - { - await ConnectToSonarQube("7.2.0.0"); - - const string response = "{\"settings\":[{\"key\":\"sonar.global.exclusions\",\"values\":[\"**/build-wrapper-dump.json\"],\"inherited\":true}]}"; - const string request = "api/settings/values?component=my_project&keys=sonar.exclusions%2Csonar.global.exclusions%2Csonar.inclusions"; - - SetupRequest(request, response); - - var result = await service.GetServerExclusions("my_project", CancellationToken.None); - - httpClientHandler.VerifyAll(); - - result.Inclusions.Should().BeEmpty(); - result.Exclusions.Should().BeEmpty(); - result.GlobalExclusions.Should().BeEquivalentTo("**/build-wrapper-dump.json"); - } - } -} diff --git a/src/SonarQube.Client.Tests/SonarQubeService_GetIssuesBase.cs b/src/SonarQube.Client.Tests/SonarQubeService_GetIssuesBase.cs deleted file mode 100644 index 3b995a8d88..0000000000 --- a/src/SonarQube.Client.Tests/SonarQubeService_GetIssuesBase.cs +++ /dev/null @@ -1,172 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Linq; -using FluentAssertions; -using SonarQube.Client.Api; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Tests -{ - public class SonarQubeService_GetIssuesBase : SonarQubeService_TestBase - { - protected const int MaxAllowedIssues = PagedRequestBase.MaximumItemsCount; - protected const int PageSize = PagedRequestBase.MaximumPageSize; - - protected void SetupPageOfResponses(string projectName, int pageNumber, int numberOfIssues, string issueType, string[] languages=null) - { - // Sanity check of the issue types - issueType.Should().BeOneOf("CODE_SMELL", "BUG", "VULNERABILITY"); - - var startItemNumber = (pageNumber - 1) * PageSize + 1; - - var issuesJson = string.Empty; - var componentJson = string.Empty; - if (numberOfIssues > 0) - { - issuesJson = string.Join(",\n", Enumerable.Range(startItemNumber, numberOfIssues).Select(CreateIssueJson)); - componentJson = CreateComponentJson(); - } - - var languagesParameter = languages is null ? string.Empty : "&languages=" + string.Join("%2C", languages); - - SetupRequest($"api/issues/search?projects={projectName}&statuses=RESOLVED&types={issueType}{languagesParameter}&p={pageNumber}&ps={PageSize}", $@" -{{ - ""paging"": {{ - ""pageIndex"": {pageNumber}, - ""pageSize"": {PageSize}, - ""total"": 9999 - }}, - ""issues"": [ - {issuesJson} - ], - ""components"": [ - {componentJson} - ] -}}"); - } - - protected void SetupPageOfResponses(string projectName, string ruleId, string componentKey, string branch, int pageNumber, int numberOfIssues, string issueType, string[] languages=null) - { - // Sanity check of the issue types - issueType.Should().BeOneOf("CODE_SMELL", "BUG", "VULNERABILITY"); - - var startItemNumber = (pageNumber - 1) * PageSize + 1; - - var issuesJson = string.Empty; - var componentJson = string.Empty; - if (numberOfIssues > 0) - { - issuesJson = string.Join(",\n", Enumerable.Range(startItemNumber, numberOfIssues).Select(CreateIssueJson)); - componentJson = CreateComponentJson(); - } - - var languagesParameter = languages is null ? string.Empty : "&languages=" + string.Join("%2C", languages); - - SetupRequest($"api/issues/search?components={componentKey}&projects={projectName}&rules={ruleId}&branch={branch}&types={issueType}{languagesParameter}&p={pageNumber}&ps={PageSize}", $@" -{{ - ""paging"": {{ - ""pageIndex"": {pageNumber}, - ""pageSize"": {PageSize}, - ""total"": 9999 - }}, - ""issues"": [ - {issuesJson} - ], - ""components"": [ - {componentJson} - ] -}}"); - } - - protected void SetupPagesOfResponses(string projectName, int numberOfIssues, string issueType, string[] languages = null) - { - var pageNumber = 1; - var remainingIssues = numberOfIssues; - while (remainingIssues > 0) - { - var issuesOnNewPage = Math.Min(remainingIssues, PageSize); - SetupPageOfResponses(projectName, pageNumber, issuesOnNewPage, issueType, languages); - - pageNumber++; - remainingIssues -= issuesOnNewPage; - } - } - - protected void SetupPagesOfResponses(string projectName, string ruleId, string componentKey, string branch, int numberOfIssues, string issueType) - { - var pageNumber = 1; - var remainingIssues = numberOfIssues; - while (remainingIssues > 0) - { - var issuesOnNewPage = Math.Min(remainingIssues, PageSize); - SetupPageOfResponses(projectName, ruleId, componentKey, branch, pageNumber, issuesOnNewPage, issueType); - - pageNumber++; - remainingIssues -= issuesOnNewPage; - } - } - - protected static string CreateIssueJson(int number) => - "{ " + - $"\"key\": \"{number}\", " + - "\"rule\": \"csharpsquid:S1075\", " + - "\"component\": \"shared:shared:2B470B7D-D47B-4E41-B105-D3938E196082:Program.cs\", " + - "\"project\": \"shared\", " + - "\"subProject\": \"shared:shared:2B470B7D-D47B-4E41-B105-D3938E196082\", " + - "\"line\": 36, " + - "\"hash\": \"0dcbf3b077bacc9fdbd898ff3b587085\", " + - "\"status\": \"RESOLVED\", " + - "\"message\": \"Refactor your code not to use hardcoded absolute paths or URIs.\" " + - "}"; - - protected static string CreateComponentJson() => - // "key" should be the same as the "component" field in the issues from CreateIssueJson() - @" -{ - ""organization"": ""default-organization"", - ""key"": ""shared:shared:2B470B7D-D47B-4E41-B105-D3938E196082:Program.cs"", - ""uuid"": ""AWg8adNk_JurIR2zdSvM"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""Program.cs"", - ""longName"": ""Program.cs"", - ""path"": ""Program.cs"" -}"; - - protected void checkForExpectedWarning(int itemCount, string partialText) - { - // Only expect a warning if the number of items is equal or greater than the max allowed - var expectedMessageCount = itemCount >= MaxAllowedIssues ? 1 : 0; - logger.WarningMessages.Count(x => x.Contains(partialText)).Should().Be(expectedMessageCount); - } - - protected void DumpWarningsToConsole() - { - System.Console.WriteLine("Warnings:"); - foreach (string item in logger.WarningMessages) - { - System.Console.WriteLine(item); - } - System.Console.WriteLine(""); - } - } -} diff --git a/src/SonarQube.Client.Tests/SonarQubeService_GetIssuesForComponent.cs b/src/SonarQube.Client.Tests/SonarQubeService_GetIssuesForComponent.cs deleted file mode 100644 index 2296e4404b..0000000000 --- a/src/SonarQube.Client.Tests/SonarQubeService_GetIssuesForComponent.cs +++ /dev/null @@ -1,86 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarQube.Client.Models; - -namespace SonarQube.Client.Tests -{ - [TestClass] - public class SonarQubeService_GetIssuesForComponent : SonarQubeService_GetIssuesBase - { - [TestMethod] - public void GetIssuesForComponentAsync_NotConnected() - { - // No calls to Connect - // No need to setup request, the operation should fail - - Func>> func = async () => - await service.GetIssuesForComponentAsync("simplcom", null, null, null, CancellationToken.None); - - func.Should().ThrowExactly().And - .Message.Should().Be("This operation expects the service to be connected."); - - logger.ErrorMessages.Should().Contain("The service is expected to be connected."); - } - - [TestMethod] - public async Task GetIssuesForComponentAsync_Connected_ReturnsExpected() - { - await ConnectToSonarQube("9.9.0.0"); - - string projectKey = "projectKey"; - string branch = "branch"; - string componentKey = "componentKey"; - string ruleId = "ruleId"; - - SetupPageOfResponses(projectKey, ruleId, componentKey, branch, 1, 1, "CODE_SMELL"); - SetupPageOfResponses(projectKey, ruleId, componentKey, branch, 1, 1, "BUG"); - - var result = await service.GetIssuesForComponentAsync(projectKey, branch, componentKey, ruleId, CancellationToken.None); - - result.Should().HaveCount(2); - } - - [TestMethod] - public async Task GetIssuesForComponentAsync_MaxIssues_ShowsLog() - { - await ConnectToSonarQube("9.9.0.0"); - - string projectKey = "projectKey"; - string branch = "branch"; - string componentKey = "componentKey"; - string ruleId = "ruleId"; - - SetupPagesOfResponses(projectKey, ruleId, componentKey, branch, MaxAllowedIssues, "CODE_SMELL"); - SetupPagesOfResponses(projectKey, ruleId, componentKey, branch, MaxAllowedIssues, "BUG"); - - var result = await service.GetIssuesForComponentAsync(projectKey, branch, componentKey, ruleId, CancellationToken.None); - - result.Should().HaveCount(MaxAllowedIssues * 2); - - DumpWarningsToConsole(); - - httpClientHandler.VerifyAll(); - - checkForExpectedWarning(MaxAllowedIssues, "code smells"); - checkForExpectedWarning(MaxAllowedIssues, "bugs"); - } - } -} diff --git a/src/SonarQube.Client.Tests/SonarQubeService_GetProjectBranchesAsync.cs b/src/SonarQube.Client.Tests/SonarQubeService_GetProjectBranchesAsync.cs deleted file mode 100644 index 845176a0bb..0000000000 --- a/src/SonarQube.Client.Tests/SonarQubeService_GetProjectBranchesAsync.cs +++ /dev/null @@ -1,109 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Net; -using System.Net.Http; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Tests -{ - [TestClass] - public class SonarQubeService_GetProjectBranchesAsync : SonarQubeService_TestBase - { - [TestMethod] - public async Task Get_V6_6() - { - await ConnectToSonarQube("6.6.0.0"); - - const string response = @"{ - ""branches"": [ - { - ""name"": ""feature/foo"", - ""isMain"": false, - ""type"": ""SHORT"", - ""status"": { - ""qualityGateStatus"": ""OK"" - }, - ""analysisDate"": ""2017-04-03T13:37:00+0100"", - ""excludedFromPurge"": false - }, - { - ""name"": ""master"", - ""isMain"": true, - ""type"": ""LONG"", - ""status"": { - ""qualityGateStatus"": ""ERROR"" - }, - ""analysisDate"": ""2018-12-01T01:15:42-0400"", - ""excludedFromPurge"": true - } - ] -}"; - SetupRequest("api/project_branches/list?project=my_project", - response); - - var result = await service.GetProjectBranchesAsync("my_project", CancellationToken.None); - - httpClientHandler.VerifyAll(); - - result.Should().HaveCount(2); - result[0].Name.Should().Be("feature/foo"); - result[0].IsMain.Should().Be(false); - result[0].Type.Should().Be("SHORT"); - result[0].LastAnalysisTimestamp.Should().Be(new DateTimeOffset(2017, 4, 3, 13, 37, 0, TimeSpan.FromHours(1))); - - result[1].Name.Should().Be("master"); - result[1].IsMain.Should().Be(true); - result[1].Type.Should().Be("LONG"); - result[1].LastAnalysisTimestamp.Should().Be(new DateTimeOffset(2018, 12, 1, 1, 15, 42, TimeSpan.FromHours(-4))); - } - - [TestMethod] - public async Task Get_NotFound() - { - await ConnectToSonarQube("6.6.0.0"); - - SetupRequest("api/project_branches/list?project=missing", "", HttpStatusCode.NotFound); - - Func>> func = async () => - await service.GetProjectBranchesAsync("missing", CancellationToken.None); - - func.Should().ThrowExactly().And - .Message.Should().Be("Response status code does not indicate success: 404 (Not Found)."); - - httpClientHandler.VerifyAll(); - } - - [TestMethod] - public void Get_NotConnected() - { - // No calls to Connect - // No need to setup request, the operation should fail - - Func>> func = async () => - await service.GetProjectBranchesAsync("any", CancellationToken.None); - - func.Should().ThrowExactly().And - .Message.Should().Be("This operation expects the service to be connected."); - - logger.ErrorMessages.Should().Contain("The service is expected to be connected."); - } - } -} diff --git a/src/SonarQube.Client.Tests/SonarQubeService_GetRulesAsync.cs b/src/SonarQube.Client.Tests/SonarQubeService_GetRulesAsync.cs deleted file mode 100644 index 7b86441e5c..0000000000 --- a/src/SonarQube.Client.Tests/SonarQubeService_GetRulesAsync.cs +++ /dev/null @@ -1,453 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarQube.Client.Models; - -namespace SonarQube.Client.Tests -{ - [TestClass] - public class SonarQubeService_GetRulesAsync : SonarQubeService_TestBase - { - [TestMethod] - public async Task GetRulesAsync_Old_Active_SonarQubeResponse() - { - await ConnectToSonarQube(); - - SetupRequest("api/rules/search?activation=true&qprofile=quality-profile-1&f=repo%2CinternalKey%2Cparams%2Cactives&p=1&ps=500", @" -{ - ""total"": 4, - ""p"": 1, - ""ps"": 500, - ""rules"": [ - { - ""key"": ""csharpsquid:S2225"", - ""repo"": ""csharpsquid"", - ""params"": [], - ""type"": ""BUG"" - }, - { - ""key"": ""csharpsquid:S4524"", - ""repo"": ""csharpsquid"", - ""params"": [], - ""type"": ""VULNERABILITY"" - }, - { - ""key"": ""csharpsquid:S2342"", - ""repo"": ""csharpsquid"", - ""params"": [ - { - ""key"": ""format"", - ""defaultValue"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$"", - ""type"": ""STRING"" - }, - { - ""key"": ""flagsAttributeFormat"", - ""defaultValue"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$"", - ""type"": ""STRING"" - } - ], - ""type"": ""CODE_SMELL"" - }, - ], - ""actives"": { - ""csharpsquid:S2225"": [ - { - ""qProfile"": ""quality-profile-1"", - ""inherit"": ""NONE"", - ""severity"": ""MAJOR"", - ""params"": [], - ""createdAt"": ""2019-01-10T14:00:14+0100"", - ""updatedAt"": ""2019-01-10T14:00:14+0100"" - } - ], - ""csharpsquid:S4524"": [ - { - ""qProfile"": ""quality-profile-1"", - ""inherit"": ""NONE"", - ""severity"": ""CRITICAL"", - ""params"": [], - ""createdAt"": ""2019-01-10T14:00:14+0100"", - ""updatedAt"": ""2019-01-10T14:00:14+0100"" - } - ], - ""csharpsquid:S2342"": [ - { - ""qProfile"": ""quality-profile-1"", - ""inherit"": ""NONE"", - ""severity"": ""MINOR"", - ""params"": [ - { - ""key"": ""format"", - ""value"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$"" - }, - { - ""key"": ""flagsAttributeFormat"", - ""value"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$"" - } - ], - ""createdAt"": ""2019-01-10T14:00:14+0100"", - ""updatedAt"": ""2019-01-10T14:00:14+0100"" - } - ] - }, - ""qProfiles"": { - ""quality-profile-1"": { - ""name"": ""Sonar way"", - ""lang"": ""cs"", - ""langName"": ""C#"" - } - } -} -"); - - var result = await service.GetRulesAsync(true, "quality-profile-1", CancellationToken.None); - - httpClientHandler.VerifyAll(); - - result.Should().HaveCount(3); - result.Select(r => r.Key).Should().ContainInOrder(new[] { "S2225", "S4524", "S2342" }); - result.Select(r => r.RepositoryKey).Should().Contain(new[] { "csharpsquid", "csharpsquid", "csharpsquid" }); - result.Select(r => r.IsActive).Should().Contain(new[] { true, true, true }); - result.Select(r => r.Severity).Should().ContainInOrder(new[] { SonarQubeIssueSeverity.Major, SonarQubeIssueSeverity.Critical, SonarQubeIssueSeverity.Minor }); - result.Select(r => r.IssueType).Should().ContainInOrder(new[] { SonarQubeIssueType.Bug, SonarQubeIssueType.Vulnerability, SonarQubeIssueType.CodeSmell }); - - result.Select(r => r.Parameters.Count).Should().Contain(new[] { 0, 2, 0 }); - result.SelectMany(r => r.Parameters.Select(p => p.Key)).Should().ContainInOrder(new[] { "format", "flagsAttributeFormat" }); - result.SelectMany(r => r.Parameters.Select(p => p.Value)).Should().ContainInOrder(new[] { "^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$", "^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$" }); - - // All rules with empty parameters should return the same (read-only) object - // 0 = S2225, no params; 1 = S4524, no params; 2 = S2342, has params - result[0].Parameters.Should().NotBeNull(); - result[0].Parameters.Count().Should().Be(0); - result[2].Parameters.Count().Should().Be(2); - result[0].Parameters.Should().BeSameAs(result[1].Parameters); - } - - [TestMethod] - public async Task GetRulesAsync_9_6_Active_SonarQubeResponse() - { - await ConnectToSonarQube(version: "9.6.0.0"); - - SetupRequest("api/rules/search?activation=true&qprofile=quality-profile-1&f=repo%2CinternalKey%2Cparams%2Cactives&p=1&ps=500", @" -{ - ""total"": 4, - ""p"": 1, - ""ps"": 500, - ""rules"": [ - { - ""key"": ""csharpsquid:S2225"", - ""repo"": ""csharpsquid"", - ""params"": [], - ""type"": ""BUG"" - }, - { - ""key"": ""csharpsquid:S4524"", - ""repo"": ""csharpsquid"", - ""params"": [], - ""type"": ""VULNERABILITY"" - }, - { - ""key"": ""csharpsquid:S2342"", - ""repo"": ""csharpsquid"", - ""params"": [ - { - ""key"": ""format"", - ""defaultValue"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$"", - ""type"": ""STRING"" - }, - { - ""key"": ""flagsAttributeFormat"", - ""defaultValue"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$"", - ""type"": ""STRING"" - } - ], - ""type"": ""CODE_SMELL"" - }, - ], - ""actives"": { - ""csharpsquid:S2225"": [ - { - ""qProfile"": ""quality-profile-1"", - ""inherit"": ""NONE"", - ""severity"": ""MAJOR"", - ""params"": [], - ""createdAt"": ""2019-01-10T14:00:14+0100"", - ""updatedAt"": ""2019-01-10T14:00:14+0100"" - } - ], - ""csharpsquid:S4524"": [ - { - ""qProfile"": ""quality-profile-1"", - ""inherit"": ""NONE"", - ""severity"": ""CRITICAL"", - ""params"": [], - ""createdAt"": ""2019-01-10T14:00:14+0100"", - ""updatedAt"": ""2019-01-10T14:00:14+0100"" - } - ], - ""csharpsquid:S2342"": [ - { - ""qProfile"": ""quality-profile-1"", - ""inherit"": ""NONE"", - ""severity"": ""MINOR"", - ""params"": [ - { - ""key"": ""format"", - ""value"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$"" - }, - { - ""key"": ""flagsAttributeFormat"", - ""value"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$"" - } - ], - ""createdAt"": ""2019-01-10T14:00:14+0100"", - ""updatedAt"": ""2019-01-10T14:00:14+0100"" - } - ] - }, - ""qProfiles"": { - ""quality-profile-1"": { - ""name"": ""Sonar way"", - ""lang"": ""cs"", - ""langName"": ""C#"" - } - } -} -"); - - var result = await service.GetRulesAsync(true, "quality-profile-1", CancellationToken.None); - - httpClientHandler.VerifyAll(); - - result.Should().HaveCount(3); - result.Select(r => r.Key).Should().ContainInOrder(new[] { "S2225", "S4524", "S2342" }); - result.Select(r => r.RepositoryKey).Should().Contain(new[] { "csharpsquid", "csharpsquid", "csharpsquid" }); - result.Select(r => r.IsActive).Should().Contain(new[] { true, true, true }); - result.Select(r => r.Severity).Should().ContainInOrder(new[] { SonarQubeIssueSeverity.Major, SonarQubeIssueSeverity.Critical, SonarQubeIssueSeverity.Minor }); - result.Select(r => r.IssueType).Should().ContainInOrder(new[] { SonarQubeIssueType.Bug, SonarQubeIssueType.Vulnerability, SonarQubeIssueType.CodeSmell }); - - result.Select(r => r.Parameters.Count).Should().Contain(new[] { 0, 2, 0 }); - result.SelectMany(r => r.Parameters.Select(p => p.Key)).Should().ContainInOrder(new[] { "format", "flagsAttributeFormat" }); - result.SelectMany(r => r.Parameters.Select(p => p.Value)).Should().ContainInOrder(new[] { "^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$", "^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$" }); - - // All rules with empty parameters should return the same (read-only) object - // 0 = S2225, no params; 1 = S4524, no params; 2 = S2342, has params - result[0].Parameters.Should().NotBeNull(); - result[0].Parameters.Count().Should().Be(0); - result[2].Parameters.Count().Should().Be(2); - result[0].Parameters.Should().BeSameAs(result[1].Parameters); - } - - [TestMethod] - public async Task GetRulesAsync_9_5_Active_SonarQubeResponse() - { - await ConnectToSonarQube(version: "9.5.0.0"); - - SetupRequest("api/rules/search?activation=true&qprofile=quality-profile-1&f=repo%2CinternalKey%2Cparams%2Cactives&p=1&ps=500", @" -{ - ""total"": 4, - ""p"": 1, - ""ps"": 500, - ""rules"": [ - { - ""key"": ""csharpsquid:S2225"", - ""repo"": ""csharpsquid"", - ""params"": [], - ""type"": ""BUG"" - }, - { - ""key"": ""csharpsquid:S4524"", - ""repo"": ""csharpsquid"", - ""params"": [], - ""type"": ""VULNERABILITY"" - }, - { - ""key"": ""csharpsquid:S2342"", - ""repo"": ""csharpsquid"", - ""params"": [ - { - ""key"": ""format"", - ""defaultValue"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$"", - ""type"": ""STRING"" - }, - { - ""key"": ""flagsAttributeFormat"", - ""defaultValue"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$"", - ""type"": ""STRING"" - } - ], - ""type"": ""CODE_SMELL"", - }, - ], - ""actives"": { - ""csharpsquid:S2225"": [ - { - ""qProfile"": ""quality-profile-1"", - ""inherit"": ""NONE"", - ""severity"": ""MAJOR"", - ""params"": [], - ""createdAt"": ""2019-01-10T14:00:14+0100"", - ""updatedAt"": ""2019-01-10T14:00:14+0100"" - } - ], - ""csharpsquid:S4524"": [ - { - ""qProfile"": ""quality-profile-1"", - ""inherit"": ""NONE"", - ""severity"": ""CRITICAL"", - ""params"": [], - ""createdAt"": ""2019-01-10T14:00:14+0100"", - ""updatedAt"": ""2019-01-10T14:00:14+0100"" - } - ], - ""csharpsquid:S2342"": [ - { - ""qProfile"": ""quality-profile-1"", - ""inherit"": ""NONE"", - ""severity"": ""MINOR"", - ""params"": [ - { - ""key"": ""format"", - ""value"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$"" - }, - { - ""key"": ""flagsAttributeFormat"", - ""value"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$"" - } - ], - ""createdAt"": ""2019-01-10T14:00:14+0100"", - ""updatedAt"": ""2019-01-10T14:00:14+0100"" - } - ] - }, - ""qProfiles"": { - ""quality-profile-1"": { - ""name"": ""Sonar way"", - ""lang"": ""cs"", - ""langName"": ""C#"" - } - } -} -"); - - var result = await service.GetRulesAsync(true, "quality-profile-1", CancellationToken.None); - - httpClientHandler.VerifyAll(); - - result.Should().HaveCount(3); - result.Select(r => r.Key).Should().ContainInOrder(new[] { "S2225", "S4524", "S2342" }); - result.Select(r => r.RepositoryKey).Should().Contain(new[] { "csharpsquid", "csharpsquid", "csharpsquid" }); - result.Select(r => r.IsActive).Should().Contain(new[] { true, true, true }); - result.Select(r => r.Severity).Should().ContainInOrder(new[] { SonarQubeIssueSeverity.Major, SonarQubeIssueSeverity.Critical, SonarQubeIssueSeverity.Minor }); - result.Select(r => r.IssueType).Should().ContainInOrder(new[] { SonarQubeIssueType.Bug, SonarQubeIssueType.Vulnerability, SonarQubeIssueType.CodeSmell }); - - result.Select(r => r.Parameters.Count).Should().Contain(new[] { 0, 2, 0 }); - result.SelectMany(r => r.Parameters.Select(p => p.Key)).Should().ContainInOrder(new[] { "format", "flagsAttributeFormat" }); - result.SelectMany(r => r.Parameters.Select(p => p.Value)).Should().ContainInOrder(new[] { "^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$", "^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$" }); - - // All rules with empty parameters should return the same (read-only) object - // 0 = S2225, no params; 1 = S4524, no params; 2 = S2342, has params - result[0].Parameters.Should().NotBeNull(); - result[0].Parameters.Count().Should().Be(0); - result[2].Parameters.Count().Should().Be(2); - result[0].Parameters.Should().BeSameAs(result[1].Parameters); - } - - [TestMethod] - public async Task GetRulesAsync_NotActive_SonarQubeResponse() - { - await ConnectToSonarQube(); - - SetupRequest("api/rules/search?activation=false&qprofile=quality-profile-1&f=repo%2CinternalKey%2Cparams%2Cactives&p=1&ps=500", @" -{ - ""total"": 4, - ""p"": 1, - ""ps"": 500, - ""rules"": [ - { - ""key"": ""csharpsquid:S2225"", - ""repo"": ""csharpsquid"", - ""params"": [], - ""type"": ""BUG"" - }, - { - ""key"": ""csharpsquid:S4524"", - ""repo"": ""csharpsquid"", - ""params"": [], - ""type"": ""VULNERABILITY"" - }, - { - ""key"": ""csharpsquid:S2342"", - ""repo"": ""csharpsquid"", - ""params"": [ - { - ""key"": ""format"", - ""defaultValue"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$"", - ""type"": ""STRING"" - }, - { - ""key"": ""flagsAttributeFormat"", - ""defaultValue"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$"", - ""type"": ""STRING"" - } - ], - ""type"": ""CODE_SMELL"" - }, - ], - ""actives"": { }, - ""qProfiles"": { } -} -"); - - var result = await service.GetRulesAsync(false, "quality-profile-1", CancellationToken.None); - - httpClientHandler.VerifyAll(); - - result.Should().HaveCount(3); - result.Select(r => r.Key).Should().ContainInOrder(new[] { "S2225", "S4524", "S2342" }); - result.Select(r => r.RepositoryKey).Should().ContainInOrder(new[] { "csharpsquid", "csharpsquid", "csharpsquid" }); - result.Select(r => r.IsActive).Should().ContainInOrder(new[] { false, false, false }); - result.Select(r => r.Severity).Should().ContainInOrder(new[] { SonarQubeIssueSeverity.Unknown, SonarQubeIssueSeverity.Unknown, SonarQubeIssueSeverity.Unknown }); - result.Select(r => r.IssueType).Should().ContainInOrder(new[] { SonarQubeIssueType.Bug, SonarQubeIssueType.Vulnerability, SonarQubeIssueType.CodeSmell }); - - // The response contains parameter "definitions", the Parameters property contains parameter values - result.Select(r => r.Parameters.Count).Should().ContainInOrder(new[] { 0, 0, 0 }); - - // All empty parameter objects should be the same instance - result[0].Parameters.Should().BeSameAs(result[1].Parameters); - result[0].Parameters.Should().BeSameAs(result[2].Parameters); - } - - [TestMethod] - public void GetRulesAsync_NotConnected() - { - // No calls to Connect - // No need to setup request, the operation should fail - - Func>> func = async () => - await service.GetRulesAsync(true, "whatever", CancellationToken.None); - - func.Should().ThrowExactly().And - .Message.Should().Be("This operation expects the service to be connected."); - - logger.ErrorMessages.Should().Contain("The service is expected to be connected."); - } - } -} diff --git a/src/SonarQube.Client.Tests/SonarQubeService_GetSuppressedRoslynIssuesAsync.cs b/src/SonarQube.Client.Tests/SonarQubeService_GetSuppressedRoslynIssuesAsync.cs deleted file mode 100644 index 71ca390fa4..0000000000 --- a/src/SonarQube.Client.Tests/SonarQubeService_GetSuppressedRoslynIssuesAsync.cs +++ /dev/null @@ -1,297 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Net; -using System.Net.Http; -using Moq; -using SonarLint.VisualStudio.Core; -using SonarQube.Client.Models; -using static SonarQube.Client.Tests.Infra.MocksHelper; - -namespace SonarQube.Client.Tests; - -[TestClass] -public class SonarQubeService_GetSuppressedRoslynIssuesAsync : SonarQubeService_GetIssuesBase -{ - protected override Language[] MockRoslynLanguages => [Language.CSharp, Language.VBNET, Language.Cpp]; - private string[] MockRoslynServerLanguageKeys => MockRoslynLanguages.Select(x => x.ServerLanguageKey).ToArray(); - - [TestMethod] - public async Task GetSuppressedRoslynIssuesAsync_From_7_20() - { - await ConnectToSonarQube("7.2.0.0"); - - SetupRequest("api/issues/search?projects=shared&statuses=RESOLVED&types=CODE_SMELL&languages=cs%2Cvbnet%2Ccpp&p=1&ps=500", @" -{ - ""total"": 5, - ""p"": 1, - ""ps"": 100, - ""paging"": { - ""pageIndex"": 1, - ""pageSize"": 100, - ""total"": 5 - }, - ""issues"": [ - { - ""key"": ""AWg8bjfdFPFMeKWzHZ_7"", - ""rule"": ""csharpsquid:S3990"", - ""severity"": ""MAJOR"", - ""component"": ""shared:shared:2B470B7D-D47B-4E41-B105-D3938E196082"", - ""project"": ""shared"", - ""flows"": [], - ""resolution"": ""WONTFIX"", - ""status"": ""RESOLVED"", - ""message"": ""Mark this assembly with 'System.CLSCompliantAttribute'"", - ""effort"": ""1min"", - ""debt"": ""1min"", - ""author"": """", - ""tags"": [ - ""api-design"" - ], - ""creationDate"": ""2019-01-11T11:21:20+0100"", - ""updateDate"": ""2019-01-11T11:28:22+0100"", - ""type"": ""CODE_SMELL"", - ""organization"": ""default-organization"" - } - ], - ""components"": [ ] -} -"); - SetupRequest("api/issues/search?projects=shared&statuses=RESOLVED&types=BUG&languages=cs%2Cvbnet%2Ccpp&p=1&ps=500", @" -{ - ""total"": 5, - ""p"": 1, - ""ps"": 100, - ""paging"": { - ""pageIndex"": 1, - ""pageSize"": 100, - ""total"": 5 - }, - ""issues"": [ - { - ""key"": ""AWg8adcV_JurIR2zdSvR"", - ""rule"": ""csharpsquid:S1118"", - ""severity"": ""MAJOR"", - ""component"": ""shared:shared:2B470B7D-D47B-4E41-B105-D3938E196082:Program.cs"", - ""project"": ""shared"", - ""subProject"": ""shared:shared:2B470B7D-D47B-4E41-B105-D3938E196082"", - ""line"": 6, - ""hash"": ""0afa1b5e62aa3cfaf1cd9a4e63571cb5"", - ""textRange"": { - ""startLine"": 6, - ""endLine"": 6, - ""startOffset"": 10, - ""endOffset"": 17 - }, - ""flows"": [], - ""resolution"": ""WONTFIX"", - ""status"": ""RESOLVED"", - ""message"": ""Add a 'protected' constructor or the 'static' keyword to the class declaration."", - ""effort"": ""10min"", - ""debt"": ""10min"", - ""author"": """", - ""tags"": [ - ""design"" - ], - ""creationDate"": ""2019-01-11T11:16:30+0100"", - ""updateDate"": ""2019-01-11T11:26:39+0100"", - ""type"": ""BUG"", - ""organization"": ""default-organization"" - } - ], - ""components"": [ - { - ""organization"": ""default-organization"", - ""key"": ""shared:shared:2B470B7D-D47B-4E41-B105-D3938E196082:Program.cs"", - ""uuid"": ""AWg8adNk_JurIR2zdSvM"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""Program.cs"", - ""longName"": ""Program.cs"", - ""path"": ""Program.cs"" - } - ] -} -"); - - var result = await service.GetSuppressedRoslynIssuesAsync("shared", null, null, CancellationToken.None); - - result.Should().HaveCount(2); - - // Module level issues don't have FilePath, hash and line - result[0].FilePath.Should().Be(string.Empty); - result[0].Hash.Should().BeNull(); - result[0].TextRange.Should().BeNull(); - result[0].Message.Should().Be("Mark this assembly with 'System.CLSCompliantAttribute'"); - result[0].ModuleKey.Should().Be("shared:shared:2B470B7D-D47B-4E41-B105-D3938E196082"); - result[0].IsResolved.Should().BeTrue(); - result[0].RuleId.Should().Be("csharpsquid:S3990"); - result[0].Severity.Should().Be(SonarQubeIssueSeverity.Major); - - result[1].FilePath.Should().Be("Program.cs"); - result[1].Hash.Should().Be("0afa1b5e62aa3cfaf1cd9a4e63571cb5"); - result[1].TextRange.Should().BeEquivalentTo(new IssueTextRange(6, 6, 10, 17)); - result[1].Message.Should().Be("Add a 'protected' constructor or the 'static' keyword to the class declaration."); - result[1].ModuleKey.Should().Be("shared:shared:2B470B7D-D47B-4E41-B105-D3938E196082"); - result[1].IsResolved.Should().BeTrue(); - result[1].RuleId.Should().Be("csharpsquid:S1118"); - result[1].Severity.Should().Be(SonarQubeIssueSeverity.Major); - - httpClientHandler.VerifyAll(); - } - - [TestMethod] - public async Task GetSuppressedRoslynIssuesAsync_From_7_20_NotFound() - { - await ConnectToSonarQube("7.2.0.0"); - - SetupRequest("api/issues/search?projects=project1&statuses=RESOLVED&types=CODE_SMELL&languages=cs%2Cvbnet%2Ccpp&p=1&ps=500", "", HttpStatusCode.NotFound); - - Func>> func = async () => - await service.GetSuppressedRoslynIssuesAsync("project1", null, null, CancellationToken.None); - - func.Should().ThrowExactly().And - .Message.Should().Be("Response status code does not indicate success: 404 (Not Found)."); - - httpClientHandler.VerifyAll(); - } - - [TestMethod] - public async Task GetSuppressedRoslynIssuesAsync_From_7_20_Paging() - { - await ConnectToSonarQube("7.2.0.0"); - - SetupPagesOfResponses("simplcom", 1001, "CODE_SMELL", MockRoslynServerLanguageKeys); - SetupPageOfResponses("simplcom", 1, 0, "BUG", MockRoslynServerLanguageKeys); - - var result = await service.GetSuppressedRoslynIssuesAsync("simplcom", null, null, CancellationToken.None); - - result.Should().HaveCount(1001); - result.Select(i => i.FilePath).Should().Match(paths => paths.All(p => p == "Program.cs")); - - httpClientHandler.VerifyAll(); - } - - [TestMethod] - // Note: we're not testing all possible combinations because testing with the - // max number of items is relatively slow (several seconds per iteration) - [DataRow(5, 5)] // No issue types with too many issues - [DataRow(MaxAllowedIssues, 5)] // One issue type with too many issues - [DataRow(1, MaxAllowedIssues)] // Multiple issue types with too many issues - public async Task GetSuppressedRoslynIssuesAsync_From_7_20_NotifyWhenMaxIssuesReturned( - int numCodeSmells, - int numBugs) - { - await ConnectToSonarQube("7.2.0.0"); - - SetupPagesOfResponses("proj1", numCodeSmells, "CODE_SMELL", MockRoslynServerLanguageKeys); - SetupPagesOfResponses("proj1", numBugs, "BUG", MockRoslynServerLanguageKeys); - - var result = await service.GetSuppressedRoslynIssuesAsync("proj1", null, null, CancellationToken.None); - - result.Should().HaveCount( - Math.Min(MaxAllowedIssues, numCodeSmells) + - Math.Min(MaxAllowedIssues, numBugs)); - - DumpWarningsToConsole(); - - httpClientHandler.VerifyAll(); - - checkForExpectedWarning(numCodeSmells, "code smells"); - checkForExpectedWarning(numBugs, "bugs"); - } - - [TestMethod] - [DataRow("")] - [DataRow(null)] - public async Task GetSuppressedRoslynIssuesAsync_From_7_20_BranchIsNotSpecified_BranchIsNotIncludedInQueryString(string emptyBranch) - { - await ConnectToSonarQube("7.2.0.0"); - httpClientHandler.Reset(); - - SetupHttpRequest(httpClientHandler, EmptyGetIssuesResponse); - _ = await service.GetSuppressedRoslynIssuesAsync("any", emptyBranch, null, CancellationToken.None); - - // Branch is null/empty => should not be passed - var actualRequests = httpClientHandler.GetSendAsyncRequests(); - actualRequests.Should().HaveCount(2); - actualRequests.Should().NotContain(x => x.RequestUri.Query.Contains("branch")); - } - - [TestMethod] - public async Task GetSuppressedRoslynIssuesAsync_From_7_20_BranchIsSpecified_BranchIncludedInQueryString() - { - await ConnectToSonarQube("7.2.0.0"); - httpClientHandler.Reset(); - - SetupHttpRequest(httpClientHandler, EmptyGetIssuesResponse); - _ = await service.GetSuppressedRoslynIssuesAsync("any", "aBranch", null, CancellationToken.None); - - // The wrapper is expected to make three calls, for code smells, bugs, then vulnerabilities - var actualRequests = httpClientHandler.GetSendAsyncRequests(); - actualRequests.Should().HaveCount(2); - actualRequests.Should().OnlyContain(x => x.RequestUri.Query.Contains("&branch=aBranch&")); - } - - [TestMethod] - public async Task GetSuppressedRoslynIssuesAsync_From_7_20_IssueKeysAreNotSpecified_IssueKeysAreNotIncludedInQueryString() - { - await ConnectToSonarQube("7.2.0.0"); - httpClientHandler.Reset(); - - SetupHttpRequest(httpClientHandler, EmptyGetIssuesResponse); - _ = await service.GetSuppressedRoslynIssuesAsync("any", null, null, CancellationToken.None); - - // The wrapper is expected to make three calls, for code smells, bugs, then vulnerabilities - var actualRequests = httpClientHandler.GetSendAsyncRequests(); - actualRequests.Should().HaveCount(2); - actualRequests.Should().NotContain(x => x.RequestUri.Query.Contains("issues")); - } - - [TestMethod] - public async Task GetSuppressedRoslynIssuesAsync_From_7_20_IssueKeysAreSpecified_IssueKeysAreIncludedInQueryString() - { - await ConnectToSonarQube("7.2.0.0"); - httpClientHandler.Reset(); - - SetupHttpRequest(httpClientHandler, EmptyGetIssuesResponse); - _ = await service.GetSuppressedRoslynIssuesAsync("any", null, new[] { "issue1", "issue2" }, CancellationToken.None); - - // The wrapper is expected to make one call with the given issueKeys - var actualRequests = httpClientHandler.GetSendAsyncRequests(); - actualRequests.Should().ContainSingle(); - actualRequests.Should().OnlyContain(x => x.RequestUri.Query.Contains("issues=issue1%2Cissue2")); - } - - [TestMethod] - public void GetSuppressedRoslynIssuesAsync_NotConnected() - { - // No calls to Connect - // No need to setup request, the operation should fail - - Func>> func = async () => - await service.GetSuppressedRoslynIssuesAsync("simplcom", null, null, CancellationToken.None); - - func.Should().ThrowExactly().And - .Message.Should().Be("This operation expects the service to be connected."); - - logger.ErrorMessages.Should().Contain("The service is expected to be connected."); - } -} diff --git a/src/SonarQube.Client.Tests/SonarQubeService_Miscellaneous.cs b/src/SonarQube.Client.Tests/SonarQubeService_Miscellaneous.cs index 07005926c0..77a48635f6 100644 --- a/src/SonarQube.Client.Tests/SonarQubeService_Miscellaneous.cs +++ b/src/SonarQube.Client.Tests/SonarQubeService_Miscellaneous.cs @@ -44,9 +44,6 @@ public void SonarQubeService_Ctor_ArgumentChecks() action = () => new SonarQubeService(string.Empty, null, languageProvider); action.Should().ThrowExactly().And.ParamName.Should().Be("logger"); - - action = () => new SonarQubeService(string.Empty, logger, null); - action.Should().ThrowExactly().And.ParamName.Should().Be("languageProvider"); } [TestMethod] @@ -81,7 +78,7 @@ public async Task SonarQubeService_UsesFactorySelector(string serverUrl, bool is var messageHandlerFactory = new Mock(); messageHandlerFactory.Setup(x => x.Create(connectionInfo.ServerUri)).Returns(Mock.Of()); - var testSubject = new SonarQubeService(messageHandlerFactory.Object, "user-agent", logger, Substitute.For(), selectorMock.Object, null); + var testSubject = new SonarQubeService(messageHandlerFactory.Object, "user-agent", logger, selectorMock.Object); await testSubject.ConnectAsync(connectionInfo, CancellationToken.None); selectorMock.Verify(x => x.Select(isSonarCloud, logger), Times.Once); @@ -111,7 +108,7 @@ public async Task SonarQubeService_FactorySelector_RequestsNewFactoryOnEachConne var messageHandlerFactory = new Mock(); messageHandlerFactory.Setup(x => x.Create(It.IsAny())).Returns(Mock.Of()); - var testSubject = new SonarQubeService(messageHandlerFactory.Object, "user-agent", logger, Substitute.For(), selectorMock.Object, null); + var testSubject = new SonarQubeService(messageHandlerFactory.Object, "user-agent", logger, selectorMock.Object); // 1. Connect to SonarQube await testSubject.ConnectAsync(sonarQubeConnectionInfo, CancellationToken.None); diff --git a/src/SonarQube.Client.Tests/SonarQubeService_PagedRequestTests.cs b/src/SonarQube.Client.Tests/SonarQubeService_PagedRequestTests.cs deleted file mode 100644 index e589cad08a..0000000000 --- a/src/SonarQube.Client.Tests/SonarQubeService_PagedRequestTests.cs +++ /dev/null @@ -1,267 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using SonarQube.Client.Requests; -using SonarQube.Client.Tests.Infra; -using static SonarQube.Client.Tests.Infra.MocksHelper; - -namespace SonarQube.Client.Tests -{ - [TestClass] - public class SonarQubeService_PagedRequestTests - { - private Mock messageHandler; - private TestLogger logger; - private HttpClient client; - - [TestInitialize] - public void TestInitialize() - { - messageHandler = new Mock(MockBehavior.Strict); - client = new HttpClient(messageHandler.Object) - { - BaseAddress = new Uri(ValidBaseAddress) - }; - logger = new TestLogger(); - } - - [TestMethod] - [DataRow(true)] - [DataRow(false)] - public async Task MaxItemCount_RetrievesTheSpecifiedNumberOfPages(bool shouldLimit) - { - var request = new DummyPagedRequest - { - Logger = logger, - }; - - if (shouldLimit) - { - request.ItemsLimit = 1100; - } - - SetupRequest("api/dummy?p=1&ps=500", $@" -{{ - ""paging"": {{ - ""pageIndex"": 1, - ""pageSize"": 4, - ""total"": 1000 - }}, - ""dummyResponses"": [ - {string.Join(",\n", Enumerable.Range(1, 500).Select(i => $@"{{ ""key"": ""{i}"", ""name"": ""Name{i}"" }}"))} - ] -}}"); - - SetupRequest("api/dummy?p=2&ps=500", $@" -{{ - ""paging"": {{ - ""pageIndex"": 2, - ""pageSize"": 4, - ""total"": 1000 - }}, - ""dummyResponses"": [ - {string.Join(",\n", Enumerable.Range(500, 500).Select(i => $@"{{ ""key"": ""{i}"", ""name"": ""Name{i}"" }}"))} - ] -}}"); - - SetupRequest("api/dummy?p=3&ps=500", $@" -{{ - ""paging"": {{ - ""pageIndex"": 3, - ""pageSize"": 4, - ""total"": 1000 - }}, - ""dummyResponses"": [ - {string.Join(",\n", Enumerable.Range(1000, 500).Select(i => $@"{{ ""key"": ""{i}"", ""name"": ""Name{i}"" }}"))} - ] -}}"); - - SetupRequest("api/dummy?p=4&ps=500", $@" -{{ - ""paging"": {{ - ""pageIndex"": 3, - ""pageSize"": 4, - ""total"": 1000 - }}, - ""dummyResponses"": [ - {string.Join(",\n", Enumerable.Range(1500, 100).Select(i => $@"{{ ""key"": ""{i}"", ""name"": ""Name{i}"" }}"))} - ] -}}"); - - var expectedCount = shouldLimit ? 1100 : 1600; - - var result = await request.InvokeAsync(client, CancellationToken.None); - result.Should().HaveCount(expectedCount); - } - - [TestMethod] - public async Task PageSize_IsRespected() - { - var request = new DummyPagedRequest - { - Logger = logger, - PageSize = 22 - }; - - // If the correct page size isn't passed in the query string then the - // test will fail because the mock http client won't have a response - // matches the supplied URL. - SetupRequest("api/dummy?p=1&ps=22", @" -{ - ""dummyResponses"": [ - { - ""key"": ""cs"", - ""name"": ""C#"", - }, - { - ""key"": ""vbnet"", - ""name"": ""VB.NET"" - } - ] -}"); - var result = await request.InvokeAsync(client, CancellationToken.None); - result.Should().HaveCount(2); - } - - [TestMethod] - public async Task RequestPageSize_IsRespected() - { - var request = new DummyPagedRequest - { - Logger = logger, - PageSize = 4 - }; - - // Full page of data - SetupRequest("api/dummy?p=1&ps=4", $@" -{{ - ""paging"": {{ - ""pageIndex"": 1, - ""pageSize"": 4, - ""total"": 10 - }}, - ""dummyResponses"": [ - {string.Join(",\n", Enumerable.Range(1, 4).Select(i => $@"{{ ""key"": ""{i}"", ""name"": ""Name{i}"" }}"))} - ] -}}"); - - // Full page of data - SetupRequest("api/dummy?p=2&ps=4", $@" -{{ - ""paging"": {{ - ""pageIndex"": 2, - ""pageSize"": 4, - ""total"": 10 - }}, - ""dummyResponses"": [ - {string.Join(",\n", Enumerable.Range(5, 4).Select(i => $@"{{ ""key"": ""{i}"", ""name"": ""Name{i}"" }}"))} - ] -}}"); - - // Partial page of data - should stop - SetupRequest("api/dummy?p=3&ps=4", $@" -{{ - ""paging"": {{ - ""pageIndex"": 3, - ""pageSize"": 2, - ""total"": 10 - }}, - ""dummyResponses"": [ - {string.Join(",\n", Enumerable.Range(9, 2).Select(i => $@"{{ ""key"": ""{i}"", ""name"": ""Name{i}"" }}"))} - ] -}}"); - - // Additional page - should never be requested - SetupRequest("api/dummy?p=4&ps=4", $@" -{{ - ""paging"": {{ - ""pageIndex"": 4, - ""pageSize"": 2, - ""total"": 10 - }}, - ""dummyResponses"": [ - {string.Join(",\n", Enumerable.Range(11, 2).Select(i => $@"{{ ""key"": ""{i}"", ""name"": ""Name{i}"" }}"))} - ] -}}"); - - var result = await request.InvokeAsync(client, CancellationToken.None); - - // Should stop after two pages of data - result.Should().HaveCount(10); - } - - private void SetupRequest(string relativePath, string response, HttpStatusCode statusCode = HttpStatusCode.OK) => - SetupHttpRequest(messageHandler, relativePath, response, statusCode, ValidBaseAddress); - - #region Dummy request and response objects - - private class DummyPagedRequest : PagedRequestBase - { - protected override string Path => "api/dummy"; - - protected override DummyObject[] ParseResponse(string response) => - JObject.Parse(response)["dummyResponses"] - .ToObject() - .Select(ToDummyObject) - .ToArray(); - - - private DummyObject ToDummyObject(DummyResponse dummyResponse) => - new DummyObject(dummyResponse.Key, dummyResponse.Name); - - private class DummyResponse - { - [JsonProperty("key")] - public string Key { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - } - } - - /// - /// Data object returned by the request - /// - private class DummyObject - { - public DummyObject(string key, string name) - { - Key = key; - Name = name; - } - - public string Key { get; } - public string Name { get; } - } - - #endregion - } -} diff --git a/src/SonarQube.Client.Tests/SonarQubeService_SearchFilesByName.cs b/src/SonarQube.Client.Tests/SonarQubeService_SearchFilesByName.cs deleted file mode 100644 index 98f593c456..0000000000 --- a/src/SonarQube.Client.Tests/SonarQubeService_SearchFilesByName.cs +++ /dev/null @@ -1,176 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Net; -using System.Net.Http; - -namespace SonarQube.Client.Tests -{ - [TestClass] - public class SonarQubeService_SearchFilesByName : SonarQubeService_TestBase - { - [TestMethod] - public void SearchFilesByName_NotConnected() - { - // No calls to Connect - // No need to setup request, the operation should fail - - Func action = async () => await service.SearchFilesByNameAsync("projectKey", "branch", "fileName", CancellationToken.None); - - action.Should().ThrowExactly() - .WithMessage("This operation expects the service to be connected."); - - logger.ErrorMessages.Should().Contain("The service is expected to be connected."); - } - - [TestMethod] - public async Task SearchFilesByName_WrongBranch() - { - await ConnectToSonarQube("9.9.0.0"); - - var branch = "branch"; - var projectKey = "projectKey"; - var fileName = "fileName.cs"; - - var request = $"api/components/tree?component={projectKey}&branch={branch}&q={fileName}&qualifiers=FIL%2CUTS&p=1&ps=500"; - - var response = "{\r\n \"errors\": [\r\n {\r\n \"msg\": \"Component 'xyz' on branch 'none' not found\"\r\n }\r\n ]\r\n}"; - - SetupRequest(request, response, HttpStatusCode.NotFound); - - var func = async () => await service.SearchFilesByNameAsync(projectKey, branch, fileName, CancellationToken.None); - - func.Should().ThrowExactly().And - .Message.Should().Be("Response status code does not indicate success: 404 (Not Found)."); - - httpClientHandler.VerifyAll(); - } - - [TestMethod] - public async Task SearchFilesByName_SingleFile_Returns() - { - await ConnectToSonarQube("9.9.0.0"); - - var branch = "master"; - var projectKey = "sonarlint-visualstudio"; - var fileName = "LocalHotspotStoreTests.cs"; - - var request = $"api/components/tree?component={projectKey}&branch={branch}&q={fileName}&qualifiers=FIL%2CUTS&p=1&ps=500"; - - var response = @"{ - ""paging"": { - ""pageIndex"": 1, - ""pageSize"": 100, - ""total"": 1 - }, - ""baseComponent"": { - ""organization"": ""sonarsource"", - ""key"": ""sonarlint-visualstudio"", - ""name"": ""SonarLint for Visual Studio"", - ""qualifier"": ""TRK"", - ""tags"": [ - ""dotnet"" - ], - ""visibility"": ""public"" - }, - ""components"": [ - { - ""organization"": ""sonarsource"", - ""key"": ""sonarlint-visualstudio:src/IssueViz.Security.UnitTests/Hotspots/LocalHotspotStoreTests.cs"", - ""name"": ""LocalHotspotStoreTests.cs"", - ""qualifier"": ""UTS"", - ""path"": ""src/IssueViz.Security.UnitTests/Hotspots/LocalHotspotStoreTests.cs"", - ""language"": ""cs"" - } - ] -}"; - SetupRequest(request, response); - - var filePaths = await service.SearchFilesByNameAsync(projectKey, branch, fileName, CancellationToken.None); - - filePaths.Should().ContainSingle(); - filePaths.Should().HaveElementAt(0, @"src\IssueViz.Security.UnitTests\Hotspots\LocalHotspotStoreTests.cs"); - } - - [TestMethod] - public async Task SearchFilesByName_MultipleFiles_Returns() - { - await ConnectToSonarQube("9.9.0.0"); - - var branch = "master"; - var projectKey = "sonarlint-visualstudio"; - var fileName = "LocalHotspotStoreTests.cs"; - - var request = $"api/components/tree?component={projectKey}&branch={branch}&q={fileName}&qualifiers=FIL%2CUTS&p=1&ps=500"; - - var response = @"{ - ""paging"": { - ""pageIndex"": 1, - ""pageSize"": 100, - ""total"": 1 - }, - ""baseComponent"": { - ""organization"": ""sonarsource"", - ""key"": ""sonarlint-visualstudio"", - ""name"": ""SonarLint for Visual Studio"", - ""qualifier"": ""TRK"", - ""tags"": [ - ""dotnet"" - ], - ""visibility"": ""public"" - }, - ""components"": [ - { - ""organization"": ""sonarsource"", - ""key"": ""sonarlint-visualstudio:path0/LocalHotspotStoreTests.cs"", - ""name"": ""LocalHotspotStoreTests.cs"", - ""qualifier"": ""UTS"", - ""path"": ""path0/LocalHotspotStoreTests.cs"", - ""language"": ""cs"" - }, - { - ""organization"": ""sonarsource"", - ""key"": ""sonarlint-visualstudio:path1/LocalHotspotStoreTests.cs"", - ""name"": ""LocalHotspotStoreTests.cs"", - ""qualifier"": ""UTS"", - ""path"": ""path1/LocalHotspotStoreTests.cs"", - ""language"": ""cs"" - }, - { - ""organization"": ""sonarsource"", - ""key"": ""sonarlint-visualstudio:path2/LocalHotspotStoreTests.cs"", - ""name"": ""LocalHotspotStoreTests.cs"", - ""qualifier"": ""UTS"", - ""path"": ""path2/LocalHotspotStoreTests.cs"", - ""language"": ""cs"" - }, - ] -}"; - SetupRequest(request, response); - - var filePaths = await service.SearchFilesByNameAsync(projectKey, branch, fileName, CancellationToken.None); - - filePaths.Should().HaveCount(3); - filePaths.Should().HaveElementAt(0, @"path0\LocalHotspotStoreTests.cs"); - filePaths.Should().HaveElementAt(1, @"path1\LocalHotspotStoreTests.cs"); - filePaths.Should().HaveElementAt(2, @"path2\LocalHotspotStoreTests.cs"); - } - } -} diff --git a/src/SonarQube.Client.Tests/SonarQubeService_TestBase.cs b/src/SonarQube.Client.Tests/SonarQubeService_TestBase.cs index 6e41791b8e..2cf6a1c209 100644 --- a/src/SonarQube.Client.Tests/SonarQubeService_TestBase.cs +++ b/src/SonarQube.Client.Tests/SonarQubeService_TestBase.cs @@ -28,7 +28,6 @@ using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; using SonarQube.Client.Models; -using SonarQube.Client.Models.ServerSentEvents; using SonarQube.Client.Requests; using SonarQube.Client.Tests.Infra; @@ -45,13 +44,12 @@ public class SonarQubeService_TestBase // Note: can't be protected because the interfaces are internal internal IRequestFactorySelector requestFactorySelector; - internal Mock sseStreamFactory; private const string DefaultBasePath = "http://localhost/"; protected const string UserAgent = "the-test-user-agent/1.0"; - protected virtual Language[] MockRoslynLanguages { get; } + protected virtual RoslynLanguage[] MockRoslynLanguages { get; } [TestInitialize] public void TestInitialize() @@ -71,7 +69,6 @@ public void TestInitialize() proxyDetector = new Mock(); requestFactorySelector = new RequestFactorySelector(); - sseStreamFactory = new Mock(); ResetService(); } @@ -127,7 +124,7 @@ protected void ResetService() protected internal virtual SonarQubeService CreateTestSubject() { - return new SonarQubeService(httpClientHandlerFactory.Object, UserAgent, logger, languageProvider, requestFactorySelector, sseStreamFactory.Object); + return new SonarQubeService(httpClientHandlerFactory.Object, UserAgent, logger, requestFactorySelector); } private static IUsernameAndPasswordCredentials MockBasicAuthCredentials(string userName, SecureString password) diff --git a/src/SonarQube.Client.Tests/TestResources/IssuesProtobufResponse b/src/SonarQube.Client.Tests/TestResources/IssuesProtobufResponse deleted file mode 100644 index b2582e9d8c..0000000000 Binary files a/src/SonarQube.Client.Tests/TestResources/IssuesProtobufResponse and /dev/null differ diff --git a/src/SonarQube.Client.Tests/packages.config b/src/SonarQube.Client.Tests/packages.config index 08a5bff550..92ccca978c 100644 --- a/src/SonarQube.Client.Tests/packages.config +++ b/src/SonarQube.Client.Tests/packages.config @@ -2,7 +2,6 @@ - diff --git a/src/SonarQube.Client.Tests/packages.lock.json b/src/SonarQube.Client.Tests/packages.lock.json index 011570e3b7..bd1e0dee0d 100644 --- a/src/SonarQube.Client.Tests/packages.lock.json +++ b/src/SonarQube.Client.Tests/packages.lock.json @@ -14,12 +14,6 @@ "resolved": "0.11.4", "contentHash": "zSCkwOgc5OyfMfEeMr9x0K7WCDf8i6VdF2RtCLN/4m6iebTtJQdeoJ9IS4/RyYHuLUYjrm0sd+siWbaSvSzRYQ==" }, - "Google.Protobuf": { - "type": "Direct", - "requested": "[3.6.1, )", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, "Microsoft.NET.Test.Sdk": { "type": "Direct", "requested": "[16.6.1, )", @@ -83,11 +77,6 @@ "resolved": "5.1.1", "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==" }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "Microsoft.CodeCoverage": { "type": "Transitive", "resolved": "16.6.1", @@ -165,8 +154,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/SonarQube.Client/Api/Common/ApiExtensions.cs b/src/SonarQube.Client/Api/Common/ApiExtensions.cs deleted file mode 100644 index 1ac812a4b6..0000000000 --- a/src/SonarQube.Client/Api/Common/ApiExtensions.cs +++ /dev/null @@ -1,61 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json.Linq; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Api.Common -{ - internal static class ApiExtensions - { - internal static ILookup GetComponentKeyPathLookup(this JObject root) - { - var components = root["components"] == null - ? Array.Empty() - : root["components"].ToObject(); - - return components - .Where(c => c.IsFile) - .ToLookup(c => c.Key, c => c.Path); // Using a Lookup because it does not throw, unlike the Dictionary - } - - internal static IEnumerable GetComponentPathList(this JObject root) - { - var components = root["components"] == null - ? Array.Empty() - : root["components"].ToObject(); - - return components.Select(c => c.Path); - } - - internal static IssueTextRange ToIssueTextRange(this ServerIssueTextRange serverIssueTextRange) - { - return serverIssueTextRange == null - ? null - : new IssueTextRange(serverIssueTextRange.StartLine, - serverIssueTextRange.EndLine, - serverIssueTextRange.StartOffset, - serverIssueTextRange.EndOffset); - } - } -} diff --git a/src/SonarQube.Client/Api/DefaultConfiguration.cs b/src/SonarQube.Client/Api/DefaultConfiguration.cs index eb742b4d07..2821988726 100644 --- a/src/SonarQube.Client/Api/DefaultConfiguration.cs +++ b/src/SonarQube.Client/Api/DefaultConfiguration.cs @@ -28,19 +28,8 @@ public static RequestFactory ConfigureSonarQube(RequestFactory requestFactory) { requestFactory .RegisterRequest("2.1") - .RegisterRequest("2.6") .RegisterRequest("3.3") - .RegisterRequest("5.2") - .RegisterRequest("5.5") - .RegisterRequest("6.3") - .RegisterRequest("6.5") - .RegisterRequest("6.6") - .RegisterRequest("6.6") - .RegisterRequest>("7.2") - .RegisterRequest("7.2") - .RegisterRequest("9.4") - .RegisterRequest("10.2") - .RegisterRequest("9.9"); + .RegisterRequest("6.6"); return requestFactory; } @@ -50,14 +39,7 @@ public static UnversionedRequestFactory ConfigureSonarCloud(UnversionedRequestFa requestFactory .RegisterRequest() .RegisterRequest() - .RegisterRequest() - .RegisterRequest() - .RegisterRequest() - .RegisterRequest() - .RegisterRequest() - .RegisterRequest>() - .RegisterRequest() - .RegisterRequest(); + .RegisterRequest(); return requestFactory; } diff --git a/src/SonarQube.Client/Api/IGetProjectBranchesRequest.cs b/src/SonarQube.Client/Api/IGetProjectBranchesRequest.cs deleted file mode 100644 index 04f8bc08f3..0000000000 --- a/src/SonarQube.Client/Api/IGetProjectBranchesRequest.cs +++ /dev/null @@ -1,33 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarQube.Client.Models; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api -{ - /// - /// Returns branch information for the specified project - /// - public interface IGetProjectBranchesRequest : IRequest - { - string ProjectKey { get; set; } - } -} diff --git a/src/SonarQube.Client/Api/IGetQualityProfilesRequest.cs b/src/SonarQube.Client/Api/IGetQualityProfilesRequest.cs deleted file mode 100644 index eb9e6e35ab..0000000000 --- a/src/SonarQube.Client/Api/IGetQualityProfilesRequest.cs +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarQube.Client.Models; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api -{ - public interface IGetQualityProfilesRequest : IRequest - { - string ProjectKey { get; set; } - - string OrganizationKey { get; set; } - } -} diff --git a/src/SonarQube.Client/Api/ISearchFilesByNameRequest.cs b/src/SonarQube.Client/Api/ISearchFilesByNameRequest.cs deleted file mode 100644 index 14d5b66ea5..0000000000 --- a/src/SonarQube.Client/Api/ISearchFilesByNameRequest.cs +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api -{ - public interface ISearchFilesByNameRequest : IPagedRequest - { - string ProjectKey { get; set; } - string BranchName { get; set; } - string FileName { get; set; } - } -} diff --git a/src/SonarQube.Client/Api/V10_2/GetIssuesWithCCTRequest.cs b/src/SonarQube.Client/Api/V10_2/GetIssuesWithCCTRequest.cs deleted file mode 100644 index 4eb1a62add..0000000000 --- a/src/SonarQube.Client/Api/V10_2/GetIssuesWithCCTRequest.cs +++ /dev/null @@ -1,77 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Linq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using SonarQube.Client.Api.Common; -using SonarQube.Client.Api.V7_20; -using SonarQube.Client.Api.V9_6; -using SonarQube.Client.Helpers; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Api.V10_2 -{ - /// - /// This class does not support component-based search. See for more information - /// - internal class GetIssuesWithCCTRequest : GetIssuesWithContextRequest - { - protected override SonarQubeIssue[] ParseResponse(string response) - { - var root = JObject.Parse(response); - - // This is a paged request so ParseResponse will be called once for each "page" - // of the response. However, we expect each page to be self-contained, so we want - // to rebuild the lookup each time. - componentKeyPathLookup = root.GetComponentKeyPathLookup(); - - return root["issues"] - .ToObject() - .Select(ToSonarQubeIssue) - .ToArray(); - } - - private SonarQubeIssue ToSonarQubeIssue(ServerIssueWithCCT issue) => new SonarQubeIssue(issue.IssueKey, - ComputePath(issue.Component), - issue.Hash, - issue.Message, - ComputeModuleKey(issue), - issue.CompositeRuleKey, - issue.Status == "RESOLVED", - SonarQubeIssueSeverityConverter.Convert(issue.Severity), - issue.CreationDate, - issue.UpdateDate, - issue.TextRange.ToIssueTextRange(), - ToIssueFlows(issue.Flows), - issue.ContextKey, - CleanCodeTaxonomyHelpers.ToSonarQubeCleanCodeAttribute(issue.CleanCodeAttribute), - CleanCodeTaxonomyHelpers.ToDefaultImpacts(issue.Impacts)); - - private class ServerIssueWithCCT : ServerIssue - { - [JsonProperty("cleanCodeAttribute")] - public string CleanCodeAttribute { get; set; } - - [JsonProperty("impacts")] - public ServerImpact[] Impacts { get; set; } - } - } -} diff --git a/src/SonarQube.Client/Api/V10_2/GetRulesWithCCTRequest.cs b/src/SonarQube.Client/Api/V10_2/GetRulesWithCCTRequest.cs deleted file mode 100644 index 30f3f76ccb..0000000000 --- a/src/SonarQube.Client/Api/V10_2/GetRulesWithCCTRequest.cs +++ /dev/null @@ -1,59 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using SonarQube.Client.Api.V5_50; -using SonarQube.Client.Api.V9_6; -using SonarQube.Client.Logging; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Api.V10_2 -{ - public class GetRulesWithCCTRequest : IGetRulesRequest - { - private static readonly IList ResponseFieldsOverride = GetRulesRequest.ResponseList.Concat(new[] { "cleanCodeAttribute" }).ToArray(); - - private readonly GetRulesRequest innerRequest = new GetRulesRequest(); - - public ILogger Logger { get; set; } - public int Page { get; set; } - public int PageSize { get; set; } - public int ItemsLimit { get; set; } - public bool? IsActive { get; set; } - public string QualityProfileKey { get; set; } - public string RuleKey { get; set; } - - public async Task InvokeAsync(HttpClient httpClient, CancellationToken token) - { - innerRequest.IsActive = this.IsActive; - innerRequest.QualityProfileKey = this.QualityProfileKey; - innerRequest.RuleKey = this.RuleKey; - innerRequest.Logger = this.Logger; - - innerRequest.ResponseListField = ResponseFieldsOverride; - - return await innerRequest.InvokeAsync(httpClient, token); - } - } -} diff --git a/src/SonarQube.Client/Api/V2_60/GetPropertiesRequest.cs b/src/SonarQube.Client/Api/V2_60/GetPropertiesRequest.cs deleted file mode 100644 index 582247f7a4..0000000000 --- a/src/SonarQube.Client/Api/V2_60/GetPropertiesRequest.cs +++ /dev/null @@ -1,53 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Linq; -using Newtonsoft.Json; -using SonarQube.Client.Helpers; -using SonarQube.Client.Models; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api.V2_60 -{ - public class GetPropertiesRequest : RequestBase, IGetPropertiesRequest - { - [JsonProperty("resource")] - public string ProjectKey { get; set; } - - protected override string Path => "api/properties"; - - protected override SonarQubeProperty[] ParseResponse(string response) => - JsonHelper.Deserialize(response) - .Select(ToProperty) - .ToArray(); - - private SonarQubeProperty ToProperty(PropertyResponse arg) => - new SonarQubeProperty(arg.Key, arg.Value); - - private sealed class PropertyResponse - { - [JsonProperty("key")] - public string Key { get; set; } - - [JsonProperty("value")] - public string Value { get; set; } - } - } -} diff --git a/src/SonarQube.Client/Api/V5_20/GetQualityProfilesRequest.cs b/src/SonarQube.Client/Api/V5_20/GetQualityProfilesRequest.cs deleted file mode 100644 index c78db01f77..0000000000 --- a/src/SonarQube.Client/Api/V5_20/GetQualityProfilesRequest.cs +++ /dev/null @@ -1,94 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using SonarQube.Client.Models; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api.V5_20 -{ - public class GetQualityProfilesRequest : RequestBase, IGetQualityProfilesRequest - { - [JsonProperty("projectKey")] - public virtual string ProjectKey { get; set; } - - [JsonProperty("organization")] - public virtual string OrganizationKey { get; set; } - - [JsonProperty("defaults")] - public virtual bool? Defaults => string.IsNullOrWhiteSpace(ProjectKey) ? (bool?)true : null; - - protected override string Path => "api/qualityprofiles/search"; - - public override async Task InvokeAsync(HttpClient httpClient, CancellationToken token) - { - var result = await InvokeUncheckedAsync(httpClient, token); - - if (result.StatusCode == HttpStatusCode.NotFound) - { - Logger.Info("This project has no quality profile. Downloading the default quality profile."); - - // The project has not been scanned yet, get default quality profile - ProjectKey = null; - - result = await InvokeUncheckedAsync(httpClient, token); - } - - result.EnsureSuccess(); - - return result.Value; - } - - protected override SonarQubeQualityProfile[] ParseResponse(string response) => - JObject.Parse(response)["profiles"] - .ToObject() - .Select(FromResponse) - .ToArray(); - - private static SonarQubeQualityProfile FromResponse(QualityProfileResponse response) => - new SonarQubeQualityProfile( - response.Key, response.Name, response.Language, response.IsDefault, response.LastRuleChange); - - private sealed class QualityProfileResponse - { - [JsonProperty("key")] - public string Key { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("language")] - public string Language { get; set; } - - [JsonProperty("isDefault")] - public bool IsDefault { get; set; } - - [JsonProperty("rulesUpdatedAt")] - public DateTime LastRuleChange { get; set; } - } - } -} diff --git a/src/SonarQube.Client/Api/V5_50/GetRulesRequest.cs b/src/SonarQube.Client/Api/V5_50/GetRulesRequest.cs deleted file mode 100644 index 2b7bf9d928..0000000000 --- a/src/SonarQube.Client/Api/V5_50/GetRulesRequest.cs +++ /dev/null @@ -1,154 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using SonarQube.Client.Api.Common; -using SonarQube.Client.Helpers; -using SonarQube.Client.Models; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api.V5_50 -{ - public class GetRulesRequest : PagedRequestBase, IGetRulesRequest - { - protected override string Path => "api/rules/search"; - - [JsonProperty("activation")] - public bool? IsActive { get; set; } - - [JsonProperty("qprofile")] - public string QualityProfileKey { get; set; } - - [JsonProperty("rule_key")] - public string RuleKey { get; set; } - - // Update this property if more fields are needed in the response. Have in mind - // that the field names here do not always correspond to the actual field names - // in the response! For example 'internalKey' in the request corresponds to 'key' - // in the response. The server error message (400) returns all supported fields. - // Also Make sure the field is supported in this version of the API. - // If not add a new request for API version that supports. e.g. "GetRulesWithDescriptionSectionsRequest" - - internal static readonly IList ResponseList = new List { "repo", "internalKey", "params", "actives" }; - - [JsonIgnore] - internal IList ResponseListField { get; set; } = ResponseList; - - [JsonProperty("f")] - public string ResponseFields => string.Join(",", ResponseListField); - - protected override SonarQubeRule[] ParseResponse(string response) - { - var responseJson = JObject.Parse(response); - - var activeQualityProfiles = ((JObject)responseJson["actives"]).Properties() - // Flatten the RuleKey-QualityProfile[] dictionary to make the lookup creation easier - .SelectMany( - p => p.Value - .ToObject() - .Select(q => new { Key = p.Name, QualityProfile = q })) - .ToLookup( - x => x.Key, - x => x.QualityProfile); - - return responseJson["rules"] - .ToObject() - .Select(rule => ToSonarQubeRule(rule, activeQualityProfiles[rule.Key])) - .ToArray(); - } - - private SonarQubeRule ToSonarQubeRule(RuleResponse response, - IEnumerable activeQualityProfiles) - { - var isActive = activeQualityProfiles.Any(); - - SonarQubeIssueSeverity severity; - Dictionary parameters; - if (isActive) - { - var activeQP = activeQualityProfiles.First(); - severity = SonarQubeIssueSeverityConverter.Convert(activeQP.Severity); - - // Optimisation: avoid creating objects if there are no parameters - parameters = activeQP.Parameters.Length > 0 ? - activeQP.Parameters.ToDictionary(p => p.Key, p => p.Value) : null; - } - else - { - severity = SonarQubeIssueSeverity.Unknown; - parameters = null; - } - - var issueType = SonarQubeIssueTypeConverter.Convert(response.Type); - - return new SonarQubeRule(GetRuleKey(response.Key), - response.RepositoryKey, - isActive, - severity, - CleanCodeTaxonomyHelpers.ToSonarQubeCleanCodeAttribute(response.CleanCodeAttribute), - CleanCodeTaxonomyHelpers.ToDefaultImpacts(response.Impacts), - parameters, - issueType); - } - - private static string GetRuleKey(string compositeKey) => - compositeKey.Substring(compositeKey.IndexOf(':') + 1); - - private sealed class RuleResponse - { - [JsonProperty("key")] - public string Key { get; set; } - - [JsonProperty("repo")] - public string RepositoryKey { get; set; } - - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("cleanCodeAttribute")] - public string CleanCodeAttribute { get; set; } - - [JsonProperty("impacts")] - public ServerImpact[] Impacts { get; set; } - } - - private sealed class QualityProfileResponse - { - [JsonProperty("qProfile")] - public string Key { get; set; } - - [JsonProperty("severity")] - public string Severity { get; set; } - - [JsonProperty("params")] - public ParameterResponse[] Parameters { get; set; } - } - - private sealed class ParameterResponse - { - [JsonProperty("key")] - public string Key { get; set; } - - [JsonProperty("value")] - public string Value { get; set; } - } - } -} diff --git a/src/SonarQube.Client/Api/V6_30/GetPropertiesRequest.cs b/src/SonarQube.Client/Api/V6_30/GetPropertiesRequest.cs deleted file mode 100644 index 66e7801dfa..0000000000 --- a/src/SonarQube.Client/Api/V6_30/GetPropertiesRequest.cs +++ /dev/null @@ -1,103 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using SonarQube.Client.Models; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api.V6_30 -{ - public class GetPropertiesRequest : RequestBase, IGetPropertiesRequest - { - [JsonProperty("component")] - public string ProjectKey { get; set; } - - protected override string Path => "api/settings/values"; - - protected override SonarQubeProperty[] ParseResponse(string response) => - JObject.Parse(response)["settings"] - .ToObject() - .SelectMany(ToProperties) - .ToArray(); - - public override async Task InvokeAsync(HttpClient httpClient, CancellationToken token) - { - var result = await InvokeUncheckedAsync(httpClient, token); - - if (result.StatusCode == HttpStatusCode.NotFound) - { - Logger.Info($"Project with key '{ProjectKey}' does not exist. Downloading the default properties."); - - ProjectKey = null; - - result = await InvokeUncheckedAsync(httpClient, token); - } - - result.EnsureSuccess(); - - return result.Value; - } - - private IEnumerable ToProperties(PropertyResponse arg) - { - if (arg.FieldValues != null) - { - for (int i = 0; i < arg.FieldValues.Length; i++) - { - var fieldValue = arg.FieldValues[i]; - foreach (var item in fieldValue) - { - yield return new SonarQubeProperty($"{arg.Key}.{i + 1}.{item.Key}", item.Value); - } - } - } - else if (arg.Values != null) - { - yield return new SonarQubeProperty(arg.Key, string.Join(",", arg.Values)); - } - else - { - yield return new SonarQubeProperty(arg.Key, arg.Value); - } - } - - private sealed class PropertyResponse - { - [JsonProperty("key")] - public string Key { get; set; } - - [JsonProperty("value")] - public string Value { get; set; } - - [JsonProperty("values")] - public string[] Values { get; set; } - - [JsonProperty("fieldValues")] - public Dictionary[] FieldValues { get; set; } - } - } -} diff --git a/src/SonarQube.Client/Api/V6_60/GetProjectBranchesRequest.cs b/src/SonarQube.Client/Api/V6_60/GetProjectBranchesRequest.cs deleted file mode 100644 index ebbe0d8626..0000000000 --- a/src/SonarQube.Client/Api/V6_60/GetProjectBranchesRequest.cs +++ /dev/null @@ -1,61 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Linq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using SonarQube.Client.Models; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api.V6_60 -{ - public class GetProjectBranchesRequest : RequestBase, IGetProjectBranchesRequest - { - [JsonProperty("project")] - public virtual string ProjectKey { get; set; } - - protected override string Path => "api/project_branches/list"; - - protected override SonarQubeProjectBranch[] ParseResponse(string response) => - JObject.Parse(response)["branches"] - .ToObject() - .Select(ToProjectBranch) - .ToArray(); - - private SonarQubeProjectBranch ToProjectBranch(ServerProjectBranch serverBranch) => - new SonarQubeProjectBranch(serverBranch.Name, serverBranch.IsMain, serverBranch.AnalysisDate, serverBranch.Type); - - private sealed class ServerProjectBranch - { - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("isMain")] - public bool IsMain { get; set; } - - [JsonProperty("analysisDate")] - public DateTimeOffset AnalysisDate { get; set; } - } - } -} diff --git a/src/SonarQube.Client/Api/V7_20/GetExclusionsRequest.cs b/src/SonarQube.Client/Api/V7_20/GetExclusionsRequest.cs deleted file mode 100644 index 1e2d8fbd0d..0000000000 --- a/src/SonarQube.Client/Api/V7_20/GetExclusionsRequest.cs +++ /dev/null @@ -1,85 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using SonarQube.Client.Models; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api.V7_20 -{ - public class GetExclusionsRequest : RequestBase, IGetExclusionsRequest - { - internal const string ExclusionsKey = "sonar.exclusions"; - internal const string GlobalExclusionsKey = "sonar.global.exclusions"; - internal const string InclusionsKey = "sonar.inclusions"; - - [JsonProperty("component")] - public virtual string ProjectKey { get; set; } - - [JsonProperty("keys")] - public virtual string Keys { get; } = - string.Join(",", ExclusionsKey, GlobalExclusionsKey, InclusionsKey); - - protected override string Path => "api/settings/values"; - - protected override ServerExclusions ParseResponse(string response) - { - if (string.IsNullOrEmpty(response)) - { - return new ServerExclusions(); - } - - var jsonParse = JObject.Parse(response); - - var settings = jsonParse["settings"]?.ToObject(); - - if (settings?.Any() != true) - { - return new ServerExclusions(); - } - - var exclusions = GetValues(settings, ExclusionsKey); - var globalExclusions = GetValues(settings, GlobalExclusionsKey); - var inclusions = GetValues(settings, InclusionsKey); - - return new ServerExclusions( - exclusions: exclusions, - globalExclusions: globalExclusions, - inclusions: inclusions); - } - - private static string[] GetValues(IEnumerable settings, string key) - { - return settings.SingleOrDefault(x => x.Key == key)?.Values; - } - - private sealed class Setting - { - [JsonProperty("key")] - public string Key { get; set; } - - [JsonProperty("values")] - public string[] Values { get; set; } - } - } -} diff --git a/src/SonarQube.Client/Api/V7_20/GetIssuesRequest.cs b/src/SonarQube.Client/Api/V7_20/GetIssuesRequest.cs deleted file mode 100644 index de060d3e66..0000000000 --- a/src/SonarQube.Client/Api/V7_20/GetIssuesRequest.cs +++ /dev/null @@ -1,186 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using SonarQube.Client.Api.Common; -using SonarQube.Client.Helpers; -using SonarQube.Client.Models; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api.V7_20; - -/// -/// Generic get issues class. It does not support as it is server-type-specific. -/// See and for more details. -/// -internal class GetIssuesRequest : PagedRequestBase -{ - [JsonProperty("projects")] - public virtual string ProjectKey { get; set; } - - [JsonProperty("statuses")] - public string Statuses { get; set; } - - [JsonProperty("issues")] - public string IssueKeysAsString => IssueKeys == null ? null : string.Join(",", IssueKeys); - - [JsonIgnore] - public string[] IssueKeys { get; set; } - - //These are normally comma seperated values but we need only one for now. If we need to pass more than 1 value we'll need to change the structure similar to issueKeys - //For now it's not needed. - [JsonProperty("rules")] - public string RuleId { get; set; } - - // Notes: - // 1) Branch support is not available in SQ Community edition. SQ will just ignore it. - // 2) SonarQube has supported the parameter since v6.6. However, the LTS at the point - // we added added branch-awareness to SLVS was v8.9.10. To minimise the amount of - // work on the SLVS side, we'll add branch support from SQ v7.2. - [JsonProperty("branch", DefaultValueHandling = DefaultValueHandling.Ignore), DefaultValue("")] - public string Branch { get; set; } - - // This property is not present in the IGetIssuesRequest interface, it is meant to be - // set by the GetIssuesRequestWrapper to add additional parameters to the API calls. - [JsonProperty("types")] - public string Types { get; set; } - - [JsonProperty("languages", DefaultValueHandling = DefaultValueHandling.Ignore), DefaultValue(null)] - public string Languages { get; set; } - - protected override string Path => "api/issues/search"; - - protected override SonarQubeIssue[] ParseResponse(string response) - { - var root = JObject.Parse(response); - - // This is a paged request so ParseResponse will be called once for each "page" - // of the response. However, we expect each page to be self-contained, so we want - // to rebuild the lookup each time. - componentKeyPathLookup = root.GetComponentKeyPathLookup(); - - return root["issues"] - .ToObject() - .Select(ToSonarQubeIssue) - .ToArray(); - } - - #region Json data classes -> public read-only class conversion methods - - /// - /// Lookup component key -> path for files. Each response contains normalized data, containing - /// issues and components, where each issue's "component" property points to a component with - /// the same "key". We obtain the FilePath of each issue from its corresponding component. - /// - private protected ILookup componentKeyPathLookup; - - private SonarQubeIssue ToSonarQubeIssue(ServerIssue issue) => - new SonarQubeIssue(issue.IssueKey, ComputePath(issue.Component), issue.Hash, issue.Message, ComputeModuleKey(issue), - issue.CompositeRuleKey, issue.Status == "RESOLVED", - SonarQubeIssueSeverityConverter.Convert(issue.Severity), - issue.CreationDate, - issue.UpdateDate, - issue.TextRange.ToIssueTextRange(), - ToIssueFlows(issue.Flows), - issue.ContextKey); - - private protected string ComputePath(string component) => - FilePathNormalizer.NormalizeSonarQubePath(componentKeyPathLookup[component].FirstOrDefault() ?? string.Empty); - - private protected static string ComputeModuleKey(ServerIssue issue) => - issue.SubProject ?? issue.Component; - - private protected List ToIssueFlows(ServerIssueFlow[] serverIssueFlows) => - serverIssueFlows?.Select(ToIssueFlow).ToList(); - - private protected IssueFlow ToIssueFlow(ServerIssueFlow serverIssueFlow) => - new IssueFlow(serverIssueFlow.Locations?.Select(ToIssueLocation).ToList()); - - private protected IssueLocation ToIssueLocation(ServerIssueLocation serverIssueLocation) => - new IssueLocation(ComputePath(serverIssueLocation.Component), serverIssueLocation.Component, serverIssueLocation.TextRange.ToIssueTextRange(), serverIssueLocation.Message); - - #endregion Json data classes -> public read-only class conversion methods - - #region JSON data classes - - private protected class ServerIssue - { - [JsonProperty("key")] - public string IssueKey { get; set; } - - [JsonProperty("rule")] - public string CompositeRuleKey { get; set; } - - [JsonProperty("component")] - public string Component { get; set; } - - [JsonProperty("subProject")] - public string SubProject { get; set; } - - [JsonProperty("hash")] - public string Hash { get; set; } - - [JsonProperty("message")] - public string Message { get; set; } - - [JsonProperty("status")] - public string Status { get; set; } - - [JsonProperty("severity")] - public string Severity { get; set; } - - [JsonProperty("textRange")] - public ServerIssueTextRange TextRange { get; set; } - - [JsonProperty("creationDate")] - public virtual DateTimeOffset CreationDate { get; set; } - - [JsonProperty("updateDate")] - public virtual DateTimeOffset UpdateDate { get; set; } - - [JsonProperty("flows")] - public ServerIssueFlow[] Flows { get; set; } - - [JsonProperty("ruleDescriptionContextKey")] - public string ContextKey { get; set; } - } - - private protected sealed class ServerIssueFlow - { - [JsonProperty("locations")] - public ServerIssueLocation[] Locations { get; set; } - } - - private protected sealed class ServerIssueLocation - { - [JsonProperty("component")] - public string Component { get; set; } - - [JsonProperty("textRange")] - public ServerIssueTextRange TextRange { get; set; } - - [JsonProperty("msg")] - public string Message { get; set; } - } - - #endregion // JSON data classes -} diff --git a/src/SonarQube.Client/Api/V7_20/GetIssuesRequestWrapper.cs b/src/SonarQube.Client/Api/V7_20/GetIssuesRequestWrapper.cs deleted file mode 100644 index fdb3540375..0000000000 --- a/src/SonarQube.Client/Api/V7_20/GetIssuesRequestWrapper.cs +++ /dev/null @@ -1,108 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Net.Http; -using SonarQube.Client.Logging; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Api.V7_20; - -/// -/// The SonarQube 10k API result limit problem (https://github.com/SonarSource/sonarlint-visualstudio/issues/776): -/// SonarQube will return the first 10k results from any query.The suppressed issues in large -/// projects could be more than 10k and SLVS will not hide those which are not returned by the -/// server. -/// -/// To reduce the effects of this limitation we will retrieve issues in batches by issue type. -/// The same approach is used in the other flavours of SonarLint. -/// -/// This class should be removed if/when SonarQube removes the 10k API result limitation. -/// -internal class GetIssuesRequestWrapper : IGetIssuesRequest - where T : GetIssuesWithComponentRequest, new() -{ - private readonly T innerRequest = new T(); - - public string ProjectKey { get; set; } - - public string Statuses { get; set; } - - public string Branch { get; set; } - - public string[] IssueKeys { get; set; } - - public string RuleId { get; set; } - - public string ComponentKey { get; set; } - public string Languages { get; set; } - public ILogger Logger { get; set; } - - public async Task InvokeAsync(HttpClient httpClient, CancellationToken token) - { - // Transfer all IGetIssuesRequest properties to the inner request. If more properties are - // added to IGetIssuesRequest, this block should set them. - innerRequest.ProjectKey = ProjectKey; - innerRequest.Statuses = Statuses; - innerRequest.Branch = Branch; - innerRequest.Logger = Logger; - innerRequest.IssueKeys = IssueKeys; - innerRequest.RuleId = RuleId; - innerRequest.ComponentKey = ComponentKey; - innerRequest.Languages = Languages; - - if (innerRequest.IssueKeys != null) - { - var response = await innerRequest.InvokeAsync(httpClient, token); - - return response; - } - - ResetInnerRequest(); - innerRequest.Types = "CODE_SMELL"; - var codeSmells = await innerRequest.InvokeAsync(httpClient, token); - WarnForApiLimit(codeSmells, innerRequest, "code smells"); - - ResetInnerRequest(); - innerRequest.Types = "BUG"; - var bugs = await innerRequest.InvokeAsync(httpClient, token); - WarnForApiLimit(bugs, innerRequest, "bugs"); - return codeSmells - .Concat(bugs) - .ToArray(); - } - - private void WarnForApiLimit(SonarQubeIssue[] issues, GetIssuesRequest request, string friendlyIssueType) - { - if (issues.Length == request.ItemsLimit) - { - Logger.Warning($"Sonar web API response limit reached ({request.ItemsLimit} items). Some {friendlyIssueType} might not be suppressed."); - } - } - - /// - /// For paged requests the Page property is automatically changed on each invocation. - /// We are resetting it so that our invocations for different issue types could start - /// from the first page. - /// - private void ResetInnerRequest() - { - innerRequest.Page = 1; - } -} diff --git a/src/SonarQube.Client/Api/V7_20/GetIssuesWithComponentRequest.cs b/src/SonarQube.Client/Api/V7_20/GetIssuesWithComponentRequest.cs deleted file mode 100644 index 04e62c461d..0000000000 --- a/src/SonarQube.Client/Api/V7_20/GetIssuesWithComponentRequest.cs +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using Newtonsoft.Json; - -namespace SonarQube.Client.Api.V7_20 -{ - // This class should not be used on it's own - internal abstract class GetIssuesWithComponentRequest : GetIssuesRequest - { - public abstract string ComponentKey { get; set; } - } - - /// - /// This class is used to override the query string property name for for - /// - /// - /// Reason to create this class https://github.com/SonarSource/sonarlint-visualstudio/issues/5181 - /// - internal class GetIssuesWithComponentSonarCloudRequest : GetIssuesWithComponentRequest, IGetIssuesRequest - { - [JsonProperty("componentKeys")] - public override string ComponentKey { get; set; } - } - - /// - /// This class is used to override the query string property name for for - /// - /// - /// Reason to create this class https://github.com/SonarSource/sonarlint-visualstudio/issues/5181 - /// - internal class GetIssuesWithComponentSonarQubeRequest : GetIssuesWithComponentRequest, IGetIssuesRequest - { - [JsonProperty("components")] - public override string ComponentKey { get; set; } - } -} diff --git a/src/SonarQube.Client/Api/V9_4/GetSonarLintEventStream.cs b/src/SonarQube.Client/Api/V9_4/GetSonarLintEventStream.cs deleted file mode 100644 index 1400e969f5..0000000000 --- a/src/SonarQube.Client/Api/V9_4/GetSonarLintEventStream.cs +++ /dev/null @@ -1,59 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.IO; -using System.Net.Http; -using System.Net.Http.Headers; -using Newtonsoft.Json; -using SonarLint.VisualStudio.Core; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api.V9_4; - -internal class GetSonarLintEventStream : RequestBase, IGetSonarLintEventStream -{ - private static readonly string RoslynServerLanguageKeys = string.Join(",", LanguageProvider.Instance.RoslynLanguages.Select(x => x.ServerLanguageKey)); - - protected override string Path => "api/push/sonarlint_events"; - - protected override MediaTypeWithQualityHeaderValue[] AllowedMediaTypeHeaders => new[] { MediaTypeWithQualityHeaderValue.Parse("text/event-stream") }; - - protected override async Task> ReadResponseAsync(HttpResponseMessage httpResponse) - { - var stream = await httpResponse.Content.ReadAsStreamAsync(); - - return new Result(httpResponse, stream); - } - - protected override Stream ParseResponse(string response) - { - // should not be called - throw new InvalidOperationException(); - } - - /// - /// Supports only Roslyn languages as the SSE for non-Roslyn languages are handled by SLCore. - /// - [JsonProperty("languages")] - public string Languages { get; set; } = RoslynServerLanguageKeys; - - [JsonProperty("projectKeys")] - public string ProjectKey { get; set; } -} diff --git a/src/SonarQube.Client/Api/V9_6/GetIssuesWithContextRequest.cs b/src/SonarQube.Client/Api/V9_6/GetIssuesWithContextRequest.cs deleted file mode 100644 index 07ef2534cf..0000000000 --- a/src/SonarQube.Client/Api/V9_6/GetIssuesWithContextRequest.cs +++ /dev/null @@ -1,34 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using Newtonsoft.Json; -using SonarQube.Client.Api.V7_20; - -namespace SonarQube.Client.Api.V9_6 -{ - /// - /// This class does not support component-based search. See for more information - /// - internal class GetIssuesWithContextRequest : GetIssuesRequest - { - [JsonProperty("additionalFields")] - public string AdditionalFields => "ruleDescriptionContextKey"; - } -} diff --git a/src/SonarQube.Client/Api/V9_9/SearchFilesByNameRequest.cs b/src/SonarQube.Client/Api/V9_9/SearchFilesByNameRequest.cs deleted file mode 100644 index 62115af24a..0000000000 --- a/src/SonarQube.Client/Api/V9_9/SearchFilesByNameRequest.cs +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Linq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using SonarQube.Client.Api.Common; -using SonarQube.Client.Helpers; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api.V9_9 -{ - internal class SearchFilesByNameRequest : PagedRequestBase, ISearchFilesByNameRequest - { - protected override string Path => "api/components/tree"; - - [JsonProperty("component")] - public string ProjectKey { get; set; } - - [JsonProperty("branch")] - public string BranchName { get; set; } - - [JsonProperty("q")] - public string FileName { get; set; } - - //File and Test Files - [JsonProperty("qualifiers")] - public string Qualifiers => "FIL,UTS"; - - protected override string[] ParseResponse(string response) - { - var root = JObject.Parse(response); - - return root.GetComponentPathList().Select(FilePathNormalizer.NormalizeSonarQubePath).ToArray(); - } - } -} diff --git a/src/SonarQube.Client/Helpers/CleanCodeTaxonomyHelpers.cs b/src/SonarQube.Client/Helpers/CleanCodeTaxonomyHelpers.cs deleted file mode 100644 index 790c522c68..0000000000 --- a/src/SonarQube.Client/Helpers/CleanCodeTaxonomyHelpers.cs +++ /dev/null @@ -1,48 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Generic; -using System.Linq; -using SonarQube.Client.Api.Common; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Helpers -{ - internal static class CleanCodeTaxonomyHelpers - { - internal static SonarQubeCleanCodeAttribute? ToSonarQubeCleanCodeAttribute(string value) - { - if (value == null) - { - return null; - } - - return (SonarQubeCleanCodeAttribute)Enum.Parse(typeof(SonarQubeCleanCodeAttribute), value, true); - } - - internal static Dictionary ToDefaultImpacts(ServerImpact[] impacts) - { - return impacts? - .ToDictionary(i => (SonarQubeSoftwareQuality)Enum.Parse(typeof(SonarQubeSoftwareQuality), i.SoftwareQuality, true), - i => (SonarQubeSoftwareQualitySeverity)Enum.Parse(typeof(SonarQubeSoftwareQualitySeverity), i.Severity, true)); - } - } -} diff --git a/src/SonarQube.Client/Helpers/ComponentKeyGenerator.cs b/src/SonarQube.Client/Helpers/ComponentKeyGenerator.cs deleted file mode 100644 index dc7b83ab24..0000000000 --- a/src/SonarQube.Client/Helpers/ComponentKeyGenerator.cs +++ /dev/null @@ -1,52 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.IO; - -namespace SonarQube.Client.Helpers -{ - public static class ComponentKeyGenerator - { - /// - /// Generates Sonar server component key - /// - /// Path for the local file. - /// Path for the local root of the project. Server paths are relative to this. - /// Sonar server project key - /// Component key in the format projectkey:relativepath - /// Invalid root format or the local path is not under the root. - public static string GetComponentKey(string localFilePath, string projectRootPath, string projectKey) - { - if (!Path.IsPathRooted(projectRootPath) || !projectRootPath.EndsWith(Path.DirectorySeparatorChar.ToString())) - { - throw new ArgumentException("Invalid root path format"); - } - - if (!localFilePath.StartsWith(projectRootPath)) - { - throw new ArgumentException("Local path is not under this root"); - } - - var serverFilePath = FilePathNormalizer.ServerizeWindowsPath(localFilePath.Substring(projectRootPath.Length)); - return $"{projectKey}:{serverFilePath}"; - } - } -} diff --git a/src/SonarQube.Client/ISonarQubeService.cs b/src/SonarQube.Client/ISonarQubeService.cs index 9e6bd9b151..57b056f4c9 100644 --- a/src/SonarQube.Client/ISonarQubeService.cs +++ b/src/SonarQube.Client/ISonarQubeService.cs @@ -19,7 +19,6 @@ */ using SonarQube.Client.Models; -using SonarQube.Client.Models.ServerSentEvents; namespace SonarQube.Client; @@ -36,99 +35,14 @@ public interface ISonarQubeService void Disconnect(); - Task> GetRulesAsync( - bool isActive, - string qualityProfileKey, - CancellationToken token); - - /// - /// Returns all properties for the project with the specified projectKey. If a project with such - /// key does not exist, returns the default property values for the connected SonarQube server. - /// - Task> GetAllPropertiesAsync(string projectKey, CancellationToken token); - - /// - /// Wrapper for GET api/qualityprofiles/search - /// - /// - Task> GetAllQualityProfilesAsync( - string project, - string organizationKey, - CancellationToken token); - - /// - /// Returns the suppressed issues for the specified project/branch. - /// - /// The project identifier - /// - /// (optional) The Sonar branch for which issues should be returned. If null/empty, - /// the issues for the "main" branch will be returned. - /// - /// - /// (optional) The ids of the issues to return. If empty, all issues will be returned. - Task> GetSuppressedRoslynIssuesAsync( - string projectKey, - string branch, - string[] issueKeys, - CancellationToken token); - - /// - /// Returns the issues in the specified server component with the same rule id - /// - /// The project identifier - /// - /// (optional) The Sonar branch for which issues should be returned. If null/empty, - /// the issues for the "main" branch will be returned - /// - /// The component identifier. Project/Directory/File - /// The Rule identifier. Is used to limit the number of issues in the response - /// - Task> GetIssuesForComponentAsync( - string projectKey, - string branch, - string componentKey, - string ruleId, - CancellationToken token); - Task> GetNotificationEventsAsync( string projectKey, DateTimeOffset eventsSince, CancellationToken token); - /// - /// Returns the list of server paths with matching file names - /// - /// The project identifier - /// - /// (optional) The Sonar branch for which issues should be returned. If null/empty, - /// the issues for the "main" branch will be returned - /// - /// The file name used for the search - /// - Task> SearchFilesByNameAsync( - string projectKey, - string branch, - string fileName, - CancellationToken token); - /// /// Returns the URI to view the specified issue on the server /// /// The method does not check whether the project or issue exists or not Uri GetViewIssueUrl(string projectKey, string issueKey); - - /// - /// Returns branch information for the specified project key - /// - Task> GetProjectBranchesAsync(string projectKey, CancellationToken token); - - /// - /// Returns the inclusions/exclusions - /// - Task GetServerExclusions(string projectKey, CancellationToken token); - - /// - /// Creates a new for the given - /// - Task CreateSSEStreamReader(string projectKey, CancellationToken token); } diff --git a/src/SonarQube.Client/Messages/Protobuf/.gitignore b/src/SonarQube.Client/Messages/Protobuf/.gitignore deleted file mode 100644 index 8143e15f93..0000000000 --- a/src/SonarQube.Client/Messages/Protobuf/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.cs diff --git a/src/SonarQube.Client/Messages/Protobuf/README.md b/src/SonarQube.Client/Messages/Protobuf/README.md deleted file mode 100644 index ed10249ee5..0000000000 --- a/src/SonarQube.Client/Messages/Protobuf/README.md +++ /dev/null @@ -1,2 +0,0 @@ -.proto file copied from -https://github.com/SonarSource/sonarqube/blob/master/sonar-scanner-protocol/src/main/protobuf/scanner_input.proto diff --git a/src/SonarQube.Client/Messages/Protobuf/constants.proto b/src/SonarQube.Client/Messages/Protobuf/constants.proto deleted file mode 100644 index f70c263beb..0000000000 --- a/src/SonarQube.Client/Messages/Protobuf/constants.proto +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -syntax = "proto3"; - -package SonarQube.Client.Messages; - -option java_package = "org.sonar.scanner.protocol"; -option optimize_for = SPEED; - -enum Severity { - UNSET_SEVERITY = 0; - INFO = 1; - MINOR = 2; - MAJOR = 3; - CRITICAL = 4; - BLOCKER = 5; -} \ No newline at end of file diff --git a/src/SonarQube.Client/Messages/Protobuf/scanner_input.proto b/src/SonarQube.Client/Messages/Protobuf/scanner_input.proto deleted file mode 100644 index 0a9b41d453..0000000000 --- a/src/SonarQube.Client/Messages/Protobuf/scanner_input.proto +++ /dev/null @@ -1,56 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -/* -Notes - - - "required" fields are not used as recommended by Google to keep forward-compatibility: - https://developers.google.com/protocol-buffers/docs/proto#simple - - - this is beta version of specification. It will evolve during next releases and is - not forward-compatible yet. -*/ - -syntax = "proto3"; - -package SonarQube.Client.Messages; - -import "constants.proto"; - -option java_package = "org.sonar.scanner.protocol.input"; -option optimize_for = SPEED; - -message ServerIssue { - string key = 1; - string module_key = 2; - string path = 3; - string rule_repository = 4; - string rule_key = 5; - int32 line = 6; - string msg = 7; - Severity severity = 8; - bool manual_severity = 9; - string resolution = 10; - string status = 11; - string checksum = 12; - string assignee_login = 13; - int64 creation_date = 14; - string type = 15; -} \ No newline at end of file diff --git a/src/SonarQube.Client/Models/ServerExclusions.cs b/src/SonarQube.Client/Models/ServerExclusions.cs deleted file mode 100644 index 12627ad7ae..0000000000 --- a/src/SonarQube.Client/Models/ServerExclusions.cs +++ /dev/null @@ -1,108 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Newtonsoft.Json; -using SonarLint.VisualStudio.Core.CSharpVB; - -namespace SonarQube.Client.Models -{ - public sealed class ServerExclusions : IEquatable, IFileExclusions - { - private static readonly string[] EmptyValues = Array.Empty(); - - public ServerExclusions() - : this(null, null, null) - { - } - - public ServerExclusions(IEnumerable exclusions, - IEnumerable globalExclusions, - IEnumerable inclusions) - { - Exclusions = AddPathPrefixIfNeeded(exclusions) ?? EmptyValues; - GlobalExclusions = AddPathPrefixIfNeeded(globalExclusions) ?? EmptyValues; - Inclusions = AddPathPrefixIfNeeded(inclusions) ?? EmptyValues; - } - - /// - /// Similarly to the other SL flavors, we will prefix everything with "**/" it's not already prefixed. - /// - private static string[] AddPathPrefixIfNeeded(IEnumerable paths) => - paths?.Select(path => path.StartsWith("**/") ? path : $"**/{path}").ToArray(); - - [JsonProperty("sonar.exclusions")] - public string[] Exclusions { get; set; } - - [JsonProperty("sonar.global.exclusions")] - public string[] GlobalExclusions { get; set; } - - [JsonProperty("sonar.inclusions")] - public string[] Inclusions { get; set; } - - public override string ToString() - { - return "Server Exclusions: " + - "\n Inclusions: " + string.Join(",", Inclusions) + - "\n Exclusions: " + string.Join(",", Exclusions) + - "\n Global Exclusions: " + string.Join(",", GlobalExclusions); - } - - public override bool Equals(object obj) - { - return Equals(obj as ServerExclusions); - } - - public bool Equals(ServerExclusions other) - { - if (other == null) - { - return false; - } - - if (ReferenceEquals(this, other)) - { - return true; - } - - return ToString().Equals(other.ToString()); - } - - public override int GetHashCode() - { - return ToString().GetHashCode(); - } - - public Dictionary ToDictionary() - { - var exclusions = new Dictionary - { - {"sonar.exclusions", string.Join(",", Exclusions)}, - {"sonar.global.exclusions", string.Join(",", GlobalExclusions)}, - {"sonar.inclusions", string.Join(",", Inclusions)} - }; - - return exclusions; - } - } -} diff --git a/src/SonarQube.Client/Models/ServerSentEvents/ClientContract/IIssueChangedServerEvent.cs b/src/SonarQube.Client/Models/ServerSentEvents/ClientContract/IIssueChangedServerEvent.cs deleted file mode 100644 index b555b0303d..0000000000 --- a/src/SonarQube.Client/Models/ServerSentEvents/ClientContract/IIssueChangedServerEvent.cs +++ /dev/null @@ -1,95 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - - -using System; -using System.Text; -using Newtonsoft.Json; - -namespace SonarQube.Client.Models.ServerSentEvents.ClientContract -{ - /// - /// Represents IssueChanged server event information - /// - public interface IIssueChangedServerEvent : IServerEvent - { - string ProjectKey { get; } - bool IsResolved { get; } - IBranchAndIssueKey[] BranchAndIssueKeys { get; } - - // also has Severity and Type that we don't care about - } - - public class IssueChangedServerEvent : IIssueChangedServerEvent - { - public IssueChangedServerEvent(string projectKey, bool isResolved, BranchAndIssueKey[] issues) - { - ProjectKey = projectKey ?? throw new ArgumentNullException(nameof(projectKey)); - IsResolved = isResolved; - BranchAndIssueKeys = issues ?? throw new ArgumentNullException(nameof(issues)); - } - - [JsonProperty("projectKey")] - public string ProjectKey { get; } - - [JsonProperty("resolved")] - public bool IsResolved { get; } - - [JsonProperty("issues")] - public IBranchAndIssueKey[] BranchAndIssueKeys { get; } - - // Display-friendly override for logging/debugging - public override string ToString() - { - var sb = new StringBuilder(); - sb.AppendLine($"ProjectKey: {ProjectKey}, IsResolved: {IsResolved}, Issue count: " + BranchAndIssueKeys.Length); - foreach (var item in BranchAndIssueKeys) - { - sb.AppendLine($"\tBranch: {item.BranchName}, IssueKey: {item.IssueKey}"); - } - - return sb.ToString(); - } - } - - /// - /// Tuple of the changed issue key in a specific branch - /// - public interface IBranchAndIssueKey - { - string BranchName { get; } - string IssueKey { get; } - } - - public class BranchAndIssueKey : IBranchAndIssueKey - { - public BranchAndIssueKey(string issueKey, string branchName) - { - IssueKey = issueKey ?? throw new ArgumentNullException(nameof(issueKey)); - BranchName = branchName ?? throw new ArgumentNullException(nameof(branchName)); - } - - [JsonProperty("branchName")] - public string BranchName { get; } - - [JsonProperty("issueKey")] - public string IssueKey { get; } - } -} diff --git a/src/SonarQube.Client/Models/ServerSentEvents/ClientContract/IQualityProfileEvent.cs b/src/SonarQube.Client/Models/ServerSentEvents/ClientContract/IQualityProfileEvent.cs deleted file mode 100644 index a72484f34b..0000000000 --- a/src/SonarQube.Client/Models/ServerSentEvents/ClientContract/IQualityProfileEvent.cs +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -namespace SonarQube.Client.Models.ServerSentEvents.ClientContract -{ - /// - /// Represents RuleSetChanged server event - /// - public interface IQualityProfileEvent : IServerEvent - { - // empty, as we decided to ignore the event payload & treat this as an indicator/trigger only - } - - public class QualityProfileEvent : IQualityProfileEvent - { - // empty, as we decided to ignore the event payload & treat this as an indicator/trigger only - } -} diff --git a/src/SonarQube.Client/Models/ServerSentEvents/SSEStreamReader.cs b/src/SonarQube.Client/Models/ServerSentEvents/SSEStreamReader.cs deleted file mode 100644 index 32e74cda5b..0000000000 --- a/src/SonarQube.Client/Models/ServerSentEvents/SSEStreamReader.cs +++ /dev/null @@ -1,103 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Threading.Tasks; -using Newtonsoft.Json; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; -using SonarQube.Client.Models.ServerSentEvents.ServerContract; -using System.Collections.Generic; -using SonarQube.Client.Logging; - -namespace SonarQube.Client.Models.ServerSentEvents -{ - public interface ISSEStreamReader - { - /// - /// Wraps the stream response from the server, reads from it and converts it to . - /// Will block the calling thread until an event exists or the connection is closed. - /// - /// - /// Can return null (i.e. if the underlying event type is unsupported). - /// - Task ReadAsync(); - } - - /// - /// Returns deserialized from - /// Code on the java side: https://github.com/SonarSource/sonarlint-core/blob/4f34c7c844b12e331a61c63ad7105acac41d2efd/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/push/PushApi.java - /// - internal class SSEStreamReader : ISSEStreamReader - { - private readonly ISqSSEStreamReader sqSSEStreamReader; - private readonly ILogger logger; - - private readonly IDictionary eventTypeToDataTypeMap = new Dictionary - { - {"IssueChanged", typeof(IssueChangedServerEvent)}, - {"RuleSetChanged", typeof(QualityProfileEvent)}, - }; - - public SSEStreamReader(ISqSSEStreamReader sqSSEStreamReader, ILogger logger) - { - this.sqSSEStreamReader = sqSSEStreamReader; - this.logger = logger; - } - - public async Task ReadAsync() - { - var sqEvent = await ReadNextEventAsync(); - - if (sqEvent == null || !eventTypeToDataTypeMap.ContainsKey(sqEvent.Type)) - { - return null; - } - - try - { - var deserializedEvent = JsonConvert.DeserializeObject(sqEvent.Data, eventTypeToDataTypeMap[sqEvent.Type]); - - return (IServerEvent) deserializedEvent; - } - catch (Exception ex) - { - logger.Debug("[SSEStreamReader] Failed to deserialize sq event." + - $"\n Exception: {ex}" + - $"\n Raw event type: {sqEvent.Type}" + - $"\n Raw event data: {sqEvent.Data}"); - - return null; - } - } - - private async Task ReadNextEventAsync() - { - try - { - return await sqSSEStreamReader.ReadAsync(); - } - catch (Exception) - { - sqSSEStreamReader.Dispose(); - throw; - } - } - } -} diff --git a/src/SonarQube.Client/Models/ServerSentEvents/SSEStreamReaderFactory.cs b/src/SonarQube.Client/Models/ServerSentEvents/SSEStreamReaderFactory.cs deleted file mode 100644 index 20b1d5e57f..0000000000 --- a/src/SonarQube.Client/Models/ServerSentEvents/SSEStreamReaderFactory.cs +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.IO; -using System.Threading; -using SonarQube.Client.Logging; -using SonarQube.Client.Models.ServerSentEvents.ServerContract; - -namespace SonarQube.Client.Models.ServerSentEvents -{ - internal interface ISSEStreamReaderFactory - { - ISSEStreamReader Create(Stream networkStream, CancellationToken cancellationToken); - } - - internal class SSEStreamReaderFactory : ISSEStreamReaderFactory - { - private readonly ILogger logger; - - public SSEStreamReaderFactory(ILogger logger) - { - this.logger = logger; - } - - public ISSEStreamReader Create(Stream networkStream, CancellationToken cancellationToken) - { - var sqStreamReader = new SqSSEStreamReader(new StreamReader(networkStream), cancellationToken); - var sseStreamReader = new SSEStreamReader(sqStreamReader, logger); - - return sseStreamReader; - } - } -} diff --git a/src/SonarQube.Client/Models/ServerSentEvents/ServerContract/ISqServerSentEventParser.cs b/src/SonarQube.Client/Models/ServerSentEvents/ServerContract/ISqServerSentEventParser.cs deleted file mode 100644 index c05429b84a..0000000000 --- a/src/SonarQube.Client/Models/ServerSentEvents/ServerContract/ISqServerSentEventParser.cs +++ /dev/null @@ -1,80 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Generic; -using System.Linq; - -namespace SonarQube.Client.Models.ServerSentEvents.ServerContract -{ - internal interface ISqServerSentEventParser - { - ISqServerEvent Parse(IReadOnlyList eventLines); - } - - /// - /// Parses the given string into - /// - /// - /// Parser code on the java side: https://github.com/SonarSource/sonarlint-core/blob/4f34c7c844b12e331a61c63ad7105acac41d2efd/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/stream/EventParser.java#L24\ - /// - internal class SqServerSentEventParser : ISqServerSentEventParser - { - private const string EventTypeFieldPrefix = "event: "; - private const string DataFieldPrefix = "data: "; - - public ISqServerEvent Parse(IReadOnlyList eventLines) - { - if (eventLines == null || !eventLines.Any()) - { - return null; - } - - var eventType = ParseEventType(eventLines); - var eventData = ParseEventData(eventLines); - - if (string.IsNullOrEmpty(eventType) || string.IsNullOrEmpty(eventData)) - { - return null; - } - - return new SqServerEvent(eventType, eventData); - } - - private string ParseEventType(IEnumerable eventLines) - { - var eventType = eventLines - .FirstOrDefault(x => x.StartsWith(EventTypeFieldPrefix)) - ?.Substring(EventTypeFieldPrefix.Length); - - return eventType; - } - - private string ParseEventData(IEnumerable eventLines) - { - var validDataEventLines = eventLines - .Where(x => x.StartsWith(DataFieldPrefix)) - .Select(x => x.Substring(DataFieldPrefix.Length)); - - var validEventData = string.Join("", validDataEventLines); - - return validEventData; - } - } -} diff --git a/src/SonarQube.Client/Models/ServerSentEvents/ServerContract/SqSSEStreamReader.cs b/src/SonarQube.Client/Models/ServerSentEvents/ServerContract/SqSSEStreamReader.cs deleted file mode 100644 index 8682bca666..0000000000 --- a/src/SonarQube.Client/Models/ServerSentEvents/ServerContract/SqSSEStreamReader.cs +++ /dev/null @@ -1,98 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace SonarQube.Client.Models.ServerSentEvents.ServerContract -{ - /// - /// Reads lines from the network stream and aggregates them into . - /// - /// - /// Returns aggregated or null if the stream ended or the task was cancelled. - /// Will throw if there was a problem reading from the underlying stream. - /// - internal interface ISqSSEStreamReader : IDisposable - { - Task ReadAsync(); - } - - /// - /// Code on the java side: https://github.com/SonarSource/sonarlint-core/blob/171ca4d75c24033e115a81bd7481427cd1f39f4c/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/stream/EventBuffer.java - /// - internal sealed class SqSSEStreamReader : ISqSSEStreamReader - { - private readonly StreamReader networkStreamReader; - private readonly CancellationToken cancellationToken; - private readonly ISqServerSentEventParser sqServerSentEventParser; - - public SqSSEStreamReader(StreamReader networkStreamReader, CancellationToken cancellationToken) - : this(networkStreamReader, cancellationToken, new SqServerSentEventParser()) - { - } - - internal SqSSEStreamReader(StreamReader networkStreamReader, - CancellationToken cancellationToken, - ISqServerSentEventParser sqServerSentEventParser) - { - this.networkStreamReader = networkStreamReader; - this.cancellationToken = cancellationToken; - this.sqServerSentEventParser = sqServerSentEventParser; - } - - public async Task ReadAsync() - { - var eventLines = new List(); - - while (!networkStreamReader.EndOfStream && !cancellationToken.IsCancellationRequested) - { - var line = await networkStreamReader.ReadLineAsync(); - var isEventEnd = string.IsNullOrEmpty(line); - - if (isEventEnd) - { - var parsedEvent = sqServerSentEventParser.Parse(eventLines); - - eventLines.Clear(); - - if (parsedEvent != null) - { - return parsedEvent; - } - } - else - { - eventLines.Add(line); - } - } - - return null; - } - - public void Dispose() - { - networkStreamReader.Dispose(); - } - } -} diff --git a/src/SonarQube.Client/Models/SonarQubeIssue.cs b/src/SonarQube.Client/Models/SonarQubeIssue.cs deleted file mode 100644 index bfba33340d..0000000000 --- a/src/SonarQube.Client/Models/SonarQubeIssue.cs +++ /dev/null @@ -1,135 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Generic; - -namespace SonarQube.Client.Models -{ - public class SonarQubeIssue - { - private static readonly IReadOnlyList EmptyFlows = new List().AsReadOnly(); - - public SonarQubeIssue(string issueKey, string filePath, string hash, string message, string moduleKey, string ruleId, bool isResolved, - SonarQubeIssueSeverity severity, DateTimeOffset creationTimestamp, DateTimeOffset lastUpdateTimestamp, - IssueTextRange textRange, List flows, string context = null, SonarQubeCleanCodeAttribute? cleanCodeAttribute = null, - Dictionary defaultImpacts = null) - { - IssueKey = issueKey; - FilePath = filePath; - Hash = hash; - Message = message; - ModuleKey = moduleKey; - RuleId = ruleId; - IsResolved = isResolved; - Severity = severity; - CreationTimestamp = creationTimestamp; - LastUpdateTimestamp = lastUpdateTimestamp; - TextRange = textRange; - Flows = flows ?? EmptyFlows; - Context = context; - CleanCodeAttribute = cleanCodeAttribute; - DefaultImpacts = defaultImpacts; - } - - public string IssueKey { get; } - - /// - /// Relative file path - /// - /// - /// The path is relative to the Sonar project root. - /// The path is in Windows format i.e. the directory separators are backslashes - /// - public string FilePath { get; } - - public string Hash { get; } - public string Message { get; } - public string ModuleKey { get; } - public string RuleId { get; } - - /// - /// This needs to be mutable as SLVS will update it during runtime. - /// - public bool IsResolved { get; set; } - - public SonarQubeIssueSeverity Severity { get; } - public IssueTextRange TextRange { get; } - public DateTimeOffset CreationTimestamp { get; } - public DateTimeOffset LastUpdateTimestamp { get; } - public IReadOnlyList Flows { get; } - - public string Context { get; set; } - - public SonarQubeCleanCodeAttribute? CleanCodeAttribute { get; } - - public Dictionary DefaultImpacts { get; set; } - } - - public class IssueFlow - { - private static readonly IReadOnlyList EmptyLocations = new List().AsReadOnly(); - - public IssueFlow(List locations) - { - Locations = locations ?? EmptyLocations; - } - - public IReadOnlyList Locations { get; } - } - - public class IssueLocation - { - public IssueLocation(string filePath, string moduleKey, IssueTextRange textRange, string message) - { - FilePath = filePath; - ModuleKey = moduleKey; - TextRange = textRange; - Message = message; - } - - public string FilePath { get; } - public string ModuleKey { get; } - public IssueTextRange TextRange { get; } - public string Message { get; } - - /// - /// Note: currently the hash for secondary locations is calculated - /// post-construction, so we need to be able to update this property - /// - public string Hash { get; internal set; } - } - - public class IssueTextRange - { - public IssueTextRange(int startLine, int endLine, int startOffset, int endOffset) - { - StartLine = startLine; - EndLine = endLine; - StartOffset = startOffset; - EndOffset = endOffset; - } - - public int StartLine { get; } - public int EndLine { get; } - public int StartOffset { get; } - public int EndOffset { get; } - } -} diff --git a/src/SonarQube.Client/Models/SonarQubeIssueSeverity.cs b/src/SonarQube.Client/Models/SonarQubeIssueSeverity.cs deleted file mode 100644 index 02864df683..0000000000 --- a/src/SonarQube.Client/Models/SonarQubeIssueSeverity.cs +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; - -namespace SonarQube.Client.Models -{ - public enum SonarQubeIssueSeverity - { - Unknown = 0, - Info = 1, - Minor = 2, - Major = 3, - Critical = 4, - Blocker = 5 - } - - internal static class SonarQubeIssueSeverityConverter - { - public static SonarQubeIssueSeverity Convert(string data) - { - SonarQubeIssueSeverity severity; - if (!Enum.TryParse(data, true /* ignore case */, out severity)) - { - return SonarQubeIssueSeverity.Unknown; - } - return severity; - } - } -} diff --git a/src/SonarQube.Client/Models/SonarQubeIssueType.cs b/src/SonarQube.Client/Models/SonarQubeIssueType.cs deleted file mode 100644 index 6ebc0ff5dc..0000000000 --- a/src/SonarQube.Client/Models/SonarQubeIssueType.cs +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -namespace SonarQube.Client.Models -{ - public enum SonarQubeIssueType - { - Unknown = 0, - CodeSmell = 1, - Bug = 2, - Vulnerability = 3, - SecurityHotspot = 4 - } - - internal static class SonarQubeIssueTypeConverter - { - public static SonarQubeIssueType Convert(string data) - { - switch (data?.ToUpperInvariant()) - { - case "CODE_SMELL": - return SonarQubeIssueType.CodeSmell; - case "BUG": - return SonarQubeIssueType.Bug; - case "SECURITY_HOTSPOT": - return SonarQubeIssueType.SecurityHotspot; - case "VULNERABILITY": - return SonarQubeIssueType.Vulnerability; - default: - return SonarQubeIssueType.Unknown; - } - } - } -} diff --git a/src/SonarQube.Client/Models/SonarQubeProjectBranch.cs b/src/SonarQube.Client/Models/SonarQubeProjectBranch.cs deleted file mode 100644 index 30c1711fcf..0000000000 --- a/src/SonarQube.Client/Models/SonarQubeProjectBranch.cs +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; - -namespace SonarQube.Client.Models -{ - public class SonarQubeProjectBranch - { - public string Name { get; } - public bool IsMain { get; } - public DateTimeOffset LastAnalysisTimestamp { get; } - public string Type { get; set; } - - public SonarQubeProjectBranch(string name, bool isMain, DateTimeOffset analysisDate, string type) - { - Name = name; - IsMain = isMain; - LastAnalysisTimestamp = analysisDate; - Type = type; - } - } -} diff --git a/src/SonarQube.Client/Models/SonarQubeProperty.cs b/src/SonarQube.Client/Models/SonarQubeProperty.cs deleted file mode 100644 index 501aae1c32..0000000000 --- a/src/SonarQube.Client/Models/SonarQubeProperty.cs +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -namespace SonarQube.Client.Models -{ - public class SonarQubeProperty - { - public const string TestProjectRegexKey = "sonar.cs.msbuild.testProjectPattern"; - public const string TestProjectRegexDefaultValue = @"[^\\]*test[^\\]*$"; - - public string Key { get; } - public string Value { get; } - - public SonarQubeProperty(string key, string value) - { - Key = key; - Value = value; - } - } -} diff --git a/src/SonarQube.Client/Models/SonarQubeQualityProfile.cs b/src/SonarQube.Client/Models/SonarQubeQualityProfile.cs deleted file mode 100644 index a6aedc9bc4..0000000000 --- a/src/SonarQube.Client/Models/SonarQubeQualityProfile.cs +++ /dev/null @@ -1,45 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; - -namespace SonarQube.Client.Models -{ - public class SonarQubeQualityProfile - { - // Ordinal comparer, similar to project key comparer - public static readonly StringComparer KeyComparer = StringComparer.Ordinal; - - public string Key { get; } - public string Name { get; } - public string Language { get; } - public bool IsDefault { get; } - public DateTime TimeStamp { get; } - - public SonarQubeQualityProfile(string key, string name, string language, bool isDefault, DateTime timeStamp) - { - Key = key; - Name = name; - Language = language; - IsDefault = isDefault; - TimeStamp = timeStamp; - } - } -} diff --git a/src/SonarQube.Client/Models/SonarQubeRule.cs b/src/SonarQube.Client/Models/SonarQubeRule.cs deleted file mode 100644 index c86108c41d..0000000000 --- a/src/SonarQube.Client/Models/SonarQubeRule.cs +++ /dev/null @@ -1,81 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Generic; -using System.Collections.ObjectModel; -using SonarLint.VisualStudio.Core.CSharpVB; - -namespace SonarQube.Client.Models -{ - public class SonarQubeRule : IRuleParameters - { - /// - /// Singleton to prevent creating unnecessary objects - /// - private static readonly IReadOnlyDictionary Empty = new ReadOnlyDictionary(new Dictionary()); - - public SonarQubeRule(string key, - string repositoryKey, - bool isActive, - SonarQubeIssueSeverity severity, - SonarQubeCleanCodeAttribute? cleanCodeAttribute, - Dictionary softwareQualitySeverities, - IDictionary parameters, - SonarQubeIssueType issueType) - { - Key = key; - RepositoryKey = repositoryKey; - IsActive = isActive; - Severity = severity; - CleanCodeAttribute = cleanCodeAttribute; - SoftwareQualitySeverities = softwareQualitySeverities; - IssueType = issueType; - - if (parameters == null || parameters.Count == 0) - { - Parameters = Empty; - } - else - { - Parameters = new ReadOnlyDictionary(parameters); - } - } - - public string Key { get; } - - public string RepositoryKey { get; } - - public bool IsActive { get; } - - public SonarQubeIssueSeverity Severity { get; } - - public SonarQubeCleanCodeAttribute? CleanCodeAttribute { get; } - - public Dictionary SoftwareQualitySeverities { get; } - - /// - /// When the rule is active, contains the parameters that are set in the corresponding quality profile. - /// This is empty dictionary if the rule is inactive, or does not have parameters. - /// - public IReadOnlyDictionary Parameters { get; } - - public SonarQubeIssueType IssueType { get; } - } -} diff --git a/src/SonarQube.Client/Requests/IPagedRequest.cs b/src/SonarQube.Client/Requests/IPagedRequest.cs deleted file mode 100644 index abcd18f2a3..0000000000 --- a/src/SonarQube.Client/Requests/IPagedRequest.cs +++ /dev/null @@ -1,34 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -namespace SonarQube.Client.Requests -{ - /// - /// Implement this interface for requests that return multiple items in pages. - /// - /// The type of the items returned by this request. - public interface IPagedRequest : IRequest - { - int Page { get; set; } - - int PageSize { get; set; } - int ItemsLimit { get; set; } - } -} diff --git a/src/SonarQube.Client/Requests/PagedRequestBase.cs b/src/SonarQube.Client/Requests/PagedRequestBase.cs deleted file mode 100644 index 7a9c3601dc..0000000000 --- a/src/SonarQube.Client/Requests/PagedRequestBase.cs +++ /dev/null @@ -1,91 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Newtonsoft.Json; - -namespace SonarQube.Client.Requests -{ - /// - /// Abstract implementation of IPagedRequest that automatically downloads all pages - /// from the server. - /// - /// The type of the items returned by this request. - public abstract class PagedRequestBase : RequestBase, IPagedRequest - { - private const int FirstPage = 1; - public const int MaximumPageSize = 500; - public const int MaximumItemsCount = 10000; - - [JsonIgnore] public virtual int ItemsLimit { get; set; } = MaximumItemsCount; - - [JsonProperty("p")] - public virtual int Page { get; set; } = FirstPage; - - [JsonProperty("ps")] - public virtual int PageSize { get; set; } = MaximumPageSize; - - public override async Task InvokeAsync(HttpClient httpClient, CancellationToken token) - { - Debug.Assert(ItemsLimit > 0 && ItemsLimit <= MaximumItemsCount, - $"Invalid ItemsLimit: {ItemsLimit}. Expecting a value in the range [1, {MaximumItemsCount}]"); - - var allResponseItems = new List(); - - Result pageResult; - do - { - pageResult = await InvokeUncheckedAsync(httpClient, token); - ValidateResult(pageResult, allResponseItems); - - if (pageResult.Value != null) - { - allResponseItems.AddRange(pageResult.Value); - Logger.Debug($"Received {pageResult.Value.Length} items."); - } - - Page++; - } - while (allResponseItems.Count < ItemsLimit && - pageResult.Value != null && - pageResult.Value.Length >= PageSize); - - if (allResponseItems.Count < ItemsLimit) - { - return allResponseItems.ToArray(); - } - - if (allResponseItems.Count == MaximumItemsCount) - { - Logger.Warning("Sonar web maximum API response limit reached."); - } - - return allResponseItems.Take(ItemsLimit).ToArray(); - } - - protected virtual void ValidateResult(Result pageResult, List allResponseItems) => - pageResult.EnsureSuccess(); - } -} diff --git a/src/SonarQube.Client/SonarQube.Client.csproj b/src/SonarQube.Client/SonarQube.Client.csproj index e419df30f3..898a629587 100644 --- a/src/SonarQube.Client/SonarQube.Client.csproj +++ b/src/SonarQube.Client/SonarQube.Client.csproj @@ -10,36 +10,12 @@ - - - - - - - - - true - - - - true - - + - - - - - "$(UserProfile)/.nuget/packages/Grpc.Tools/1.4.1/tools/windows_x64/protoc.exe" - - - - - \ No newline at end of file diff --git a/src/SonarQube.Client/SonarQubeService.cs b/src/SonarQube.Client/SonarQubeService.cs index ae7acb4462..f72ec38cd2 100644 --- a/src/SonarQube.Client/SonarQubeService.cs +++ b/src/SonarQube.Client/SonarQubeService.cs @@ -18,14 +18,12 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.IO; using System.Net.Http; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; using SonarQube.Client.Api; using SonarQube.Client.Helpers; using SonarQube.Client.Models; -using SonarQube.Client.Models.ServerSentEvents; using SonarQube.Client.Requests; using ILogger = SonarQube.Client.Logging.ILogger; @@ -36,16 +34,14 @@ public class SonarQubeService : ISonarQubeService, IDisposable private const string MinSqVersionSupportingBearer = "10.4"; private readonly IHttpClientHandlerFactory httpClientHandlerFactory; private readonly ILogger logger; - private readonly ILanguageProvider languageProvider; private readonly IRequestFactorySelector requestFactorySelector; - private readonly ISSEStreamReaderFactory sseStreamReaderFactory; private readonly string userAgent; private HttpClient currentHttpClient; private ServerInfo currentServerInfo; private IRequestFactory requestFactory; public SonarQubeService(string userAgent, ILogger logger, ILanguageProvider languageProvider) - : this(new HttpClientHandlerFactory(new ProxyDetector(), logger), userAgent, logger, languageProvider, new RequestFactorySelector(), new SSEStreamReaderFactory(logger)) + : this(new HttpClientHandlerFactory(new ProxyDetector(), logger), userAgent, logger, new RequestFactorySelector()) { } @@ -53,17 +49,13 @@ public SonarQubeService(string userAgent, ILogger logger, ILanguageProvider lang IHttpClientHandlerFactory httpClientHandlerFactory, string userAgent, ILogger logger, - ILanguageProvider languageProvider, - IRequestFactorySelector requestFactorySelector, - ISSEStreamReaderFactory sseStreamReaderFactory) + IRequestFactorySelector requestFactorySelector) { this.httpClientHandlerFactory = httpClientHandlerFactory ?? throw new ArgumentNullException(nameof(httpClientHandlerFactory)); this.userAgent = userAgent ?? throw new ArgumentNullException(nameof(userAgent)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - this.languageProvider = languageProvider ?? throw new ArgumentNullException(nameof(languageProvider)); this.requestFactorySelector = requestFactorySelector; - this.sseStreamReaderFactory = sseStreamReaderFactory; } public bool IsConnected => GetServerInfo() != null; @@ -115,55 +107,6 @@ public void Disconnect() requestFactory = null; } - public async Task> GetAllPropertiesAsync(string projectKey, CancellationToken token) => - await InvokeCheckedRequestAsync( - request => - { - request.ProjectKey = projectKey; - }, - token); - - public async Task> GetAllQualityProfilesAsync(string project, string organizationKey, CancellationToken token) => - await InvokeCheckedRequestAsync( - request => - { - request.ProjectKey = project; - request.OrganizationKey = GetOrganizationKeyForWebApiCalls(organizationKey, logger); - }, - token); - - public async Task> GetSuppressedRoslynIssuesAsync( - string projectKey, - string branch, - string[] issueKeys, - CancellationToken token) => - await InvokeCheckedRequestAsync( - request => - { - request.ProjectKey = projectKey; - request.Branch = branch; - request.IssueKeys = issueKeys; - request.Languages = string.Join(",", languageProvider.RoslynLanguages.Select(x => x.ServerLanguageKey)); - request.Statuses = "RESOLVED"; // Resolved issues will be hidden in SLVS - }, - token); - - public async Task> GetIssuesForComponentAsync( - string projectKey, - string branch, - string componentKey, - string ruleId, - CancellationToken token) => - await InvokeCheckedRequestAsync( - request => - { - request.ProjectKey = projectKey; - request.Branch = branch; - request.ComponentKey = componentKey; - request.RuleId = ruleId; - }, - token); - public async Task> GetNotificationEventsAsync( string projectKey, DateTimeOffset eventsSince, @@ -176,30 +119,6 @@ await InvokeCheckedRequestAsync> SearchFilesByNameAsync( - string projectKey, - string branch, - string fileName, - CancellationToken token) => - await InvokeCheckedRequestAsync( - request => - { - request.ProjectKey = projectKey; - request.BranchName = branch; - request.FileName = fileName; - }, - token - ); - - public async Task> GetRulesAsync(bool isActive, string qualityProfileKey, CancellationToken token) => - await InvokeCheckedRequestAsync( - request => - { - request.IsActive = isActive; - request.QualityProfileKey = qualityProfileKey; - }, - token); - public Uri GetViewIssueUrl(string projectKey, string issueKey) { EnsureIsConnected(); @@ -213,32 +132,6 @@ public Uri GetViewIssueUrl(string projectKey, string issueKey) return new Uri(currentHttpClient.BaseAddress, string.Format(ViewIssueRelativeUrl, projectKey, issueKey)); } - public async Task> GetProjectBranchesAsync(string projectKey, CancellationToken token) => - await InvokeCheckedRequestAsync( - request => - { - request.ProjectKey = projectKey; - }, token); - - public async Task GetServerExclusions(string projectKey, CancellationToken token) => - await InvokeCheckedRequestAsync( - request => - { - request.ProjectKey = projectKey; - }, token); - - public async Task CreateSSEStreamReader(string projectKey, CancellationToken token) - { - var networkStream = await InvokeCheckedRequestAsync( - request => - { - request.ProjectKey = projectKey; - }, - token); - - return sseStreamReaderFactory.Create(networkStream, token); - } - /// /// Creates a new instance of the specified TRequest request, configures and invokes it and returns its response. /// diff --git a/src/SonarQube.Client/packages.config b/src/SonarQube.Client/packages.config deleted file mode 100644 index c0e2b232d4..0000000000 --- a/src/SonarQube.Client/packages.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/SonarQube.Client/packages.lock.json b/src/SonarQube.Client/packages.lock.json index d968ad250a..c5ab67976c 100644 --- a/src/SonarQube.Client/packages.lock.json +++ b/src/SonarQube.Client/packages.lock.json @@ -2,18 +2,6 @@ "version": 1, "dependencies": { ".NETFramework,Version=v4.7.2": { - "Google.Protobuf": { - "type": "Direct", - "requested": "[3.6.1, )", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Direct", - "requested": "[1.4.1, )", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "Newtonsoft.Json": { "type": "Direct", "requested": "[13.0.3, )", diff --git a/src/TestInfrastructure/DummyAnalysisIssue.cs b/src/TestInfrastructure/DummyAnalysisIssue.cs index 5f4d1199d3..41db7abcac 100644 --- a/src/TestInfrastructure/DummyAnalysisIssue.cs +++ b/src/TestInfrastructure/DummyAnalysisIssue.cs @@ -41,5 +41,5 @@ public class DummyAnalysisIssue : IAnalysisIssue public bool IsResolved { get; set; } public string IssueServerKey { get; set; } - public IReadOnlyList Fixes { get; } = Array.Empty(); + public IReadOnlyList Fixes { get; } = []; } diff --git a/src/TestInfrastructure/Helpers/DummySonarQubeIssueFactory.cs b/src/TestInfrastructure/Helpers/FakeRoslynLanguage.cs similarity index 74% rename from src/TestInfrastructure/Helpers/DummySonarQubeIssueFactory.cs rename to src/TestInfrastructure/Helpers/FakeRoslynLanguage.cs index 7067f68637..96d3d8a50d 100644 --- a/src/TestInfrastructure/Helpers/DummySonarQubeIssueFactory.cs +++ b/src/TestInfrastructure/Helpers/FakeRoslynLanguage.cs @@ -18,14 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using SonarQube.Client.Models; +using SonarLint.VisualStudio.Core; namespace SonarLint.VisualStudio.Integration.TestInfrastructure.Helpers; -public static class DummySonarQubeIssueFactory +public class FakeRoslynLanguage(string key) : RoslynLanguage(key, key, key, new PluginInfo(key, key), new RepoInfo(key), key, key, new RepoInfo(key, key)) { - public static SonarQubeIssue CreateServerIssue(bool isResolved = false) - { - return new SonarQubeIssue("issueKey", default, default, default, default, default, isResolved, default, default, default, default, default); - } + public static RoslynLanguage Instance = new FakeRoslynLanguage("fakeroslyn"); } diff --git a/src/TestInfrastructure/packages.lock.json b/src/TestInfrastructure/packages.lock.json index 40fcb507bd..d6657792a9 100644 --- a/src/TestInfrastructure/packages.lock.json +++ b/src/TestInfrastructure/packages.lock.json @@ -221,16 +221,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -1291,8 +1281,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )"