From 3cab0afcb127e87f1b194b36e0765acf1eea3042 Mon Sep 17 00:00:00 2001 From: Gabriela Trutan Date: Wed, 11 Dec 2024 16:08:56 +0100 Subject: [PATCH] SLVS-1680 Remove Roslyn suppressions file on unbind (#5887) --- .../RoslynSettingsFileSynchronizerTests.cs | 395 +++++++--------- .../RoslynSettingsFileStorageTests.cs | 428 ++++++++---------- .../RoslynSettingsFileSynchronizer.cs | 284 ++++++------ .../Resources/Strings.Designer.cs | 9 + .../Resources/Strings.resx | 3 + .../SettingsFile/RoslynSettingsFileStorage.cs | 158 ++++--- 6 files changed, 595 insertions(+), 682 deletions(-) diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/RoslynSettingsFileSynchronizerTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/RoslynSettingsFileSynchronizerTests.cs index a4a6a50f85..d70ecab238 100644 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/RoslynSettingsFileSynchronizerTests.cs +++ b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/RoslynSettingsFileSynchronizerTests.cs @@ -18,277 +18,196 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; +using System.IO; using SonarLint.VisualStudio.ConnectedMode.Suppressions; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.TestInfrastructure; using SonarLint.VisualStudio.Roslyn.Suppressions.InProcess; using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; +using SonarLint.VisualStudio.TestInfrastructure; using SonarQube.Client.Models; using EventHandler = System.EventHandler; using static SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.TestHelper; -using System.Linq; -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.InProcess +namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.InProcess; + +[TestClass] +public class RoslynSettingsFileSynchronizerTests { - [TestClass] - public class RoslynSettingsFileSynchronizerTests + private IConfigurationProvider configProvider; + private TestLogger logger; + private IRoslynSettingsFileStorage roslynSettingsFileStorage; + private IServerIssuesStore serverIssuesStore; + private ISolutionInfoProvider solutionInfoProvider; + private RoslynSettingsFileSynchronizer testSubject; + private IThreadHandling threadHandling; + private readonly BindingConfiguration connectedBindingConfiguration = CreateConnectedConfiguration("some project key"); + + [TestInitialize] + public void TestInitialize() { - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void MefCtor_CheckTypeIsNonShared() - => MefTestHelpers.CheckIsNonSharedMefComponent(); - - [TestMethod] - public void Ctor_RegisterToSuppressionsUpdateRequestedEvent() - { - var serverIssuesStore = new Mock(); + serverIssuesStore = Substitute.For(); + roslynSettingsFileStorage = Substitute.For(); + configProvider = Substitute.For(); + solutionInfoProvider = Substitute.For(); + threadHandling = Substitute.For(); + logger = new TestLogger(); + + testSubject = new RoslynSettingsFileSynchronizer(serverIssuesStore, + roslynSettingsFileStorage, + configProvider, + solutionInfoProvider, + logger, + threadHandling); + threadHandling.SwitchToBackgroundThread().Returns(new NoOpThreadHandler.NoOpAwaitable()); + } - serverIssuesStore.SetupAdd(x => x.ServerIssuesChanged += null); + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); - CreateTestSubject(serverIssuesStore: serverIssuesStore.Object); + [TestMethod] + public void MefCtor_CheckTypeIsNonShared() => MefTestHelpers.CheckIsNonSharedMefComponent(); - serverIssuesStore.VerifyAdd(x => x.ServerIssuesChanged += It.IsAny(), Times.Once()); - serverIssuesStore.VerifyNoOtherCalls(); - } + [TestMethod] + public void Ctor_RegisterToSuppressionsUpdateRequestedEvent() + { + serverIssuesStore.Received(1).ServerIssuesChanged += Arg.Any(); + VerifyServerIssuesStoreNoOtherCalls(); + } - [TestMethod] - public void Dispose_UnregisterFromSuppressionsUpdateRequestedEvent() - { - var serverIssuesStore = new Mock(); + [TestMethod] + public void Dispose_UnregisterFromSuppressionsUpdateRequestedEvent() + { + serverIssuesStore.ClearReceivedCalls(); - serverIssuesStore.SetupAdd(x => x.ServerIssuesChanged += null); - serverIssuesStore.SetupRemove(x => x.ServerIssuesChanged -= null); + testSubject.Dispose(); - var testSubject = CreateTestSubject(serverIssuesStore: serverIssuesStore.Object); + serverIssuesStore.Received(1).ServerIssuesChanged -= Arg.Any(); + VerifyServerIssuesStoreNoOtherCalls(); + } - serverIssuesStore.VerifyAdd(x => x.ServerIssuesChanged += It.IsAny(), Times.Once()); - serverIssuesStore.VerifyNoOtherCalls(); + [TestMethod] + public void OnSuppressionsUpdateRequested_StandaloneMode_StorageFileDeleted() + { + var fullSolutionFilePath = "c:\\aaa\\MySolution1.sln"; + MockSolutionInfoProvider(fullSolutionFilePath); + MockConfigProvider(BindingConfiguration.Standalone); - testSubject.Dispose(); + serverIssuesStore.ServerIssuesChanged += Raise.EventWith(); - serverIssuesStore.VerifyRemove(x => x.ServerIssuesChanged -= It.IsAny(), Times.Once()); - serverIssuesStore.VerifyNoOtherCalls(); - } + configProvider.Received(1).GetConfiguration(); + roslynSettingsFileStorage.Received(1).Delete(Path.GetFileNameWithoutExtension(fullSolutionFilePath)); + roslynSettingsFileStorage.ReceivedCalls().Should().HaveCount(1); + } - [TestMethod] - public void OnSuppressionsUpdateRequested_StandaloneMode_StorageNotUpdated() - { - var serverIssuesStore = new Mock(); - var roslynSettingsFileStorage = new Mock(); - var configProvider = CreateConfigProvider(BindingConfiguration.Standalone); - - CreateTestSubject(serverIssuesStore: serverIssuesStore.Object, - configProvider: configProvider.Object, - roslynSettingsFileStorage: roslynSettingsFileStorage.Object); - - serverIssuesStore.Raise(x=> x.ServerIssuesChanged += null, EventArgs.Empty); - - configProvider.Verify(x=> x.GetConfiguration(), Times.Once); - roslynSettingsFileStorage.Invocations.Count.Should().Be(0); - } - - [TestMethod] - [DataRow(true)] - [DataRow(false)] // should update storage even when there are no issues - public void OnSuppressionsUpdateRequested_ConnectedMode_StorageUpdated(bool hasIssues) - { - var roslynSettingsFileStorage = new Mock(); + [TestMethod] + [DataRow(true)] + [DataRow(false)] // should update storage even when there are no issues + public void OnSuppressionsUpdateRequested_ConnectedMode_StorageUpdated(bool hasIssues) + { + MockConfigProvider(connectedBindingConfiguration); + MockSolutionInfoProvider("c:\\aaa\\MySolution1.sln"); + var issues = hasIssues ? new[] { CreateSonarQubeIssue(), CreateSonarQubeIssue() } : Array.Empty(); + MockServerIssuesStore(issues); - var configuration = CreateConnectedConfiguration("some project key"); - var configProvider = CreateConfigProvider(configuration); - var solutionInfo = CreateSolutionInfoProvider("c:\\aaa\\MySolution1.sln"); + serverIssuesStore.ServerIssuesChanged += Raise.EventWith(); - var issues = hasIssues ? new[] { CreateSonarQubeIssue(), CreateSonarQubeIssue() } : Array.Empty(); - var serverIssuesStore = CreateServerIssuesStore(issues); + roslynSettingsFileStorage.Received(1).Update(Arg.Any(), "MySolution1"); + } - CreateTestSubject(serverIssuesStore: serverIssuesStore.Object, - configProvider: configProvider.Object, - roslynSettingsFileStorage: roslynSettingsFileStorage.Object, - solutionInfoProvider: solutionInfo.Object); + [TestMethod] + public async Task UpdateFileStorage_FileStorageIsUpdatedOnBackgroundThread() + { + MockConfigProvider(connectedBindingConfiguration); + var issues = new[] { CreateSonarQubeIssue() }; + MockServerIssuesStore(issues); - serverIssuesStore.Raise(x => x.ServerIssuesChanged += null, EventArgs.Empty); + await testSubject.UpdateFileStorageAsync(); - roslynSettingsFileStorage.Verify(x => x.Update(It.IsAny(), "MySolution1"), Times.Once); - } + threadHandling.Received(1).SwitchToBackgroundThread(); + roslynSettingsFileStorage.ReceivedCalls().Should().HaveCount(1); + configProvider.ReceivedCalls().Should().HaveCount(1); + roslynSettingsFileStorage.Received(1).Update(Arg.Any(), Arg.Any()); + } - [TestMethod] - public async Task UpdateFileStorage_FileStorageIsUpdatedOnBackgroundThread() + [TestMethod] + public async Task UpdateFileStorage_IssuesAreConvertedAndFiltered() + { + MockConfigProvider(connectedBindingConfiguration); + var sonarIssues = new[] { - var threadHandling = new Mock(); - threadHandling.Setup(x => x.SwitchToBackgroundThread()).Returns(new NoOpThreadHandler.NoOpAwaitable()); - - var configuration = CreateConnectedConfiguration("some project key"); - var configProvider = CreateConfigProvider(configuration); - - var roslynSettingsFileStorage = new Mock(); - - var issues = new[] { CreateSonarQubeIssue() }; - var serverIssuesStore = CreateServerIssuesStore(issues); - - var testSubject = CreateTestSubject( - serverIssuesStore: serverIssuesStore.Object, - threadHandling: threadHandling.Object, - roslynSettingsFileStorage: roslynSettingsFileStorage.Object, - configProvider: configProvider.Object); - - await testSubject.UpdateFileStorageAsync(); - - threadHandling.Verify(x => x.SwitchToBackgroundThread(), Times.Once); - - roslynSettingsFileStorage.Invocations.Count.Should().Be(1); - configProvider.Invocations.Count.Should().Be(1); - roslynSettingsFileStorage.Verify(x => x.Update(It.IsAny(), It.IsAny()), Times.Once); - } + CreateSonarQubeIssue("csharpsquid:S111"), // C# issue + CreateSonarQubeIssue("vbnet:S222"), // VB issue + CreateSonarQubeIssue("cpp:S333"), // C++ issue - ignored + CreateSonarQubeIssue("xxx:S444"), // unrecognised repo - ignored + CreateSonarQubeIssue("xxxS555"), // invalid repo key - ignored + CreateSonarQubeIssue("xxx:") // invalid repo key (no rule id) - ignored + }; + MockServerIssuesStore(sonarIssues); + RoslynSettings actualSettings = null; + roslynSettingsFileStorage.When(x => x.Update(Arg.Any(), Arg.Any())) + .Do(callInfo => actualSettings = callInfo.Arg()); + + await testSubject.UpdateFileStorageAsync(); + + actualSettings.Should().NotBeNull(); + actualSettings.SonarProjectKey.Should().Be("some project key"); + actualSettings.Suppressions.Should().NotBeNull(); + + var actualSuppressions = actualSettings.Suppressions.ToList(); + actualSuppressions.Count.Should().Be(2); + actualSuppressions[0].RoslynLanguage.Should().Be(RoslynLanguage.CSharp); + actualSuppressions[0].RoslynRuleId.Should().Be("S111"); + actualSuppressions[1].RoslynLanguage.Should().Be(RoslynLanguage.VB); + actualSuppressions[1].RoslynRuleId.Should().Be("S222"); + } - [TestMethod] - public async Task UpdateFileStorage_IssuesAreConvertedAndFiltered() - { - var configuration = CreateConnectedConfiguration("some project key"); - var configurationProvider = CreateConfigProvider(configuration); - - var sonarIssues = new [] - { - CreateSonarQubeIssue(ruleId: "csharpsquid:S111"), // C# issue - CreateSonarQubeIssue(ruleId: "vbnet:S222"),// VB issue - CreateSonarQubeIssue(ruleId: "cpp:S333"),// C++ issue - ignored - CreateSonarQubeIssue(ruleId: "xxx:S444"),// unrecognised repo - ignored - CreateSonarQubeIssue(ruleId: "xxxS555"),// invalid repo key - ignored - CreateSonarQubeIssue(ruleId: "xxx:"),// invalid repo key (no rule id) - ignored - }; - - var serverIssuesStore = CreateServerIssuesStore(sonarIssues); - - RoslynSettings actualSettings = null; - var roslynSettingsFileStorage = new Mock(); - roslynSettingsFileStorage.Setup(x => x.Update(It.IsAny(), It.IsAny())) - .Callback((x,y) => actualSettings = x); - - var testSubject = CreateTestSubject( - serverIssuesStore: serverIssuesStore.Object, - roslynSettingsFileStorage: roslynSettingsFileStorage.Object, - configProvider: configurationProvider.Object); - - await testSubject.UpdateFileStorageAsync(); - - actualSettings.Should().NotBeNull(); - actualSettings.SonarProjectKey.Should().Be("some project key"); - actualSettings.Suppressions.Should().NotBeNull(); - - var actualSuppressions = actualSettings.Suppressions.ToList(); - actualSuppressions.Count.Should().Be(2); - actualSuppressions[0].RoslynLanguage.Should().Be(RoslynLanguage.CSharp); - actualSuppressions[0].RoslynRuleId.Should().Be("S111"); - actualSuppressions[1].RoslynLanguage.Should().Be(RoslynLanguage.VB); - actualSuppressions[1].RoslynRuleId.Should().Be("S222"); - } - - [TestMethod] - public async Task UpdateFileStorage_OnlySuppressedIssuesAreInSettings() - { - var configuration = CreateConnectedConfiguration("some project key"); - var configProvider = CreateConfigProvider(configuration); - - var sonarIssues = new[] - { - CreateSonarQubeIssue(ruleId: "csharpsquid:S111", isSuppressed:false), - CreateSonarQubeIssue(ruleId: "vbnet:S222", isSuppressed:true), - CreateSonarQubeIssue(ruleId: "csharpsquid:S333", isSuppressed:true), - CreateSonarQubeIssue(ruleId: "vbnet:S444", isSuppressed:false), - }; - - var serverIssuesStore = CreateServerIssuesStore(sonarIssues); - - RoslynSettings actualSettings = null; - var roslynSettingsFileStorage = new Mock(); - roslynSettingsFileStorage.Setup(x => x.Update(It.IsAny(), It.IsAny())) - .Callback((x, y) => actualSettings = x); - - var testSubject = CreateTestSubject( - serverIssuesStore: serverIssuesStore.Object, - roslynSettingsFileStorage: roslynSettingsFileStorage.Object, - configProvider: configProvider.Object); - - await testSubject.UpdateFileStorageAsync(); - - actualSettings.Should().NotBeNull(); - actualSettings.SonarProjectKey.Should().Be("some project key"); - actualSettings.Suppressions.Should().NotBeNull(); - - var actualSuppressions = actualSettings.Suppressions.ToList(); - actualSuppressions.Count.Should().Be(2); - actualSuppressions[0].RoslynLanguage.Should().Be(RoslynLanguage.VB); - actualSuppressions[0].RoslynRuleId.Should().Be("S222"); - actualSuppressions[1].RoslynLanguage.Should().Be(RoslynLanguage.CSharp); - actualSuppressions[1].RoslynRuleId.Should().Be("S333"); - } - - private static Mock CreateConfigProvider(BindingConfiguration configuration) + [TestMethod] + public async Task UpdateFileStorage_OnlySuppressedIssuesAreInSettings() + { + MockConfigProvider(connectedBindingConfiguration); + var sonarIssues = new[] { - var configProvider = new Mock(); - configProvider.Setup(x => x.GetConfiguration()).Returns(configuration); - - return configProvider; - } + CreateSonarQubeIssue("csharpsquid:S111", isSuppressed: false), CreateSonarQubeIssue("vbnet:S222", isSuppressed: true), CreateSonarQubeIssue("csharpsquid:S333", isSuppressed: true), + CreateSonarQubeIssue("vbnet:S444", isSuppressed: false) + }; + MockServerIssuesStore(sonarIssues); + RoslynSettings actualSettings = null; + roslynSettingsFileStorage.When(x => x.Update(Arg.Any(), Arg.Any())) + .Do(callInfo => actualSettings = callInfo.Arg()); + + await testSubject.UpdateFileStorageAsync(); + + actualSettings.Should().NotBeNull(); + actualSettings.SonarProjectKey.Should().Be("some project key"); + actualSettings.Suppressions.Should().NotBeNull(); + + var actualSuppressions = actualSettings.Suppressions.ToList(); + actualSuppressions.Count.Should().Be(2); + actualSuppressions[0].RoslynLanguage.Should().Be(RoslynLanguage.VB); + actualSuppressions[0].RoslynRuleId.Should().Be("S222"); + actualSuppressions[1].RoslynLanguage.Should().Be(RoslynLanguage.CSharp); + actualSuppressions[1].RoslynRuleId.Should().Be("S333"); + } - private static Mock CreateServerIssuesStore(IEnumerable issues = null) - { - var serverIssuesStore = new Mock(); - serverIssuesStore.Setup(x => x.Get()).Returns(issues); + private void MockConfigProvider(BindingConfiguration configuration) => configProvider.GetConfiguration().Returns(configuration); - return serverIssuesStore; - } + private void MockServerIssuesStore(IEnumerable issues = null) => serverIssuesStore.Get().Returns(issues); - 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 static BindingConfiguration CreateConnectedConfiguration(string projectKey) + { + var project = new BoundServerProject("solution", projectKey, new ServerConnection.SonarQube(new Uri("http://bound"))); - private static Mock CreateSolutionInfoProvider(string fullSolutionNameToReturn) - { - var solutionInfo = new Mock(); - solutionInfo.Setup(x => x.GetFullSolutionFilePathAsync()).ReturnsAsync(fullSolutionNameToReturn); - return solutionInfo; - } - - private RoslynSettingsFileSynchronizer CreateTestSubject(IServerIssuesStore serverIssuesStore = null, - IRoslynSettingsFileStorage roslynSettingsFileStorage = null, - IConfigurationProvider configProvider = null, - ISolutionInfoProvider solutionInfoProvider = null, - IThreadHandling threadHandling = null, - ILogger logger = null) - { - serverIssuesStore ??= Mock.Of(); - roslynSettingsFileStorage ??= Mock.Of(); - configProvider ??= Mock.Of(); - solutionInfoProvider ??= CreateSolutionInfoProvider("c:\\any.sln").Object; - threadHandling ??= new NoOpThreadHandler(); - logger ??= new TestLogger(); - - return new RoslynSettingsFileSynchronizer(serverIssuesStore, - roslynSettingsFileStorage, - configProvider, - solutionInfoProvider, - logger, - threadHandling); - } + return BindingConfiguration.CreateBoundConfiguration(project, SonarLintMode.Connected, "some directory"); } + + private void MockSolutionInfoProvider(string fullSolutionNameToReturn) => solutionInfoProvider.GetFullSolutionFilePathAsync().Returns(fullSolutionNameToReturn); + + private void VerifyServerIssuesStoreNoOtherCalls() => serverIssuesStore.ReceivedCalls().Should().HaveCount(1); // no other calls } diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/RoslynSettingsFileStorageTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/RoslynSettingsFileStorageTests.cs index 4824e69dd8..81bf722952 100644 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/RoslynSettingsFileStorageTests.cs +++ b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/RoslynSettingsFileStorageTests.cs @@ -18,294 +18,254 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using System.IO.Abstractions; -using System.Linq; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Newtonsoft.Json; +using NSubstitute.ExceptionExtensions; using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.TestInfrastructure; +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 +namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests; + +[TestClass] +public class RoslynSettingsFileStorageTests { - [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() { - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport()); - } + logger = new TestLogger(); + file = Substitute.For(); + fileSystem = Substitute.For(); + testSubject = new RoslynSettingsFileStorage(logger, fileSystem); - [TestMethod] - public void Update_HasIssues_IssuesWrittenToFile() - { - var settings = new RoslynSettings - { - SonarProjectKey = "projectKey", - Suppressions = new[] { CreateIssue("issue1") } - }; - - var logger = new TestLogger(); + MockFileSystem(); + } - var file = new Mock(); - Mock fileSystem = CreateFileSystem(file); + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport()); - var fileStorage = CreateTestSubject(logger, fileSystem.Object); + [TestMethod] + public void Update_HasIssues_IssuesWrittenToFile() + { + var settings = new RoslynSettings { SonarProjectKey = "projectKey", Suppressions = new[] { CreateIssue("issue1") } }; - fileStorage.Update(settings, "a solution name"); + testSubject.Update(settings, SolutionName); - CheckFileWritten(file, settings, "a solution name"); - logger.AssertNoOutputMessages(); - } + 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 } - }; - - var logger = new TestLogger(); + [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 file = CreateFileForGet(settings); - Mock fileSystem = CreateFileSystem(file); + var actual = testSubject.Get("projectKey"); - var fileStorage = CreateTestSubject(logger, fileSystem.Object); + var issuesGotten = actual.Suppressions.ToList(); - var actual = fileStorage.Get("projectKey"); - var issuesGotten = actual.Suppressions.ToList(); + file.Received(1).ReadAllText(GetFilePath("projectKey")); + logger.AssertNoOutputMessages(); - file.Verify(f => f.ReadAllText(GetFilePath("projectKey")), Times.Once); - logger.AssertNoOutputMessages(); + issuesGotten.Count.Should().Be(2); + issuesGotten[0].RoslynRuleId.Should().Be(issue1.RoslynRuleId); + issuesGotten[1].RoslynRuleId.Should().Be(issue2.RoslynRuleId); + } - 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" }; - [TestMethod] - public void Update_SolutionNameHasInvalidChars_InvalidCharsReplaced() - { - var settings = new RoslynSettings { SonarProjectKey = "project:key" }; + testSubject.Update(settings, "my:solution"); - var logger = new TestLogger(); + CheckFileWritten(settings, "my_solution"); + logger.AssertNoOutputMessages(); + } - var file = new Mock(); - Mock fileSystem = CreateFileSystem(file); + [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")); - var fileStorage = new RoslynSettingsFileStorage(logger, fileSystem.Object); - fileStorage.Update(settings, "my:solution"); + testSubject.Update(settings, "any"); - CheckFileWritten(file, settings, "my_solution"); - logger.AssertNoOutputMessages(); - } + 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 Update_ErrorOccuredWhenWritingFile_ErrorIsLogged() - { - var settings = new RoslynSettings { SonarProjectKey = "projectKey" }; - var logger = new TestLogger(); + [TestMethod] + public void Get_ErrorOccuredWhenWritingFile_ErrorIsLoggedAndReturnsNull() + { + file.ReadAllText(GetFilePath("projectKey")).Throws(new Exception("Test Exception")); - var file = new Mock(); - file.Setup(f => f.WriteAllText(It.IsAny(), It.IsAny())).Throws(new Exception("Test Exception")); - Mock fileSystem = CreateFileSystem(file); + var actual = testSubject.Get("projectKey"); - var fileStorage = CreateTestSubject(logger, fileSystem.Object); + 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(); + } - fileStorage.Update(settings, "any"); + [TestMethod] + public void Update_HasNoIssues_FileWritten() + { + var settings = new RoslynSettings { SonarProjectKey = "projectKey", Suppressions = Enumerable.Empty() }; - 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"); - } + testSubject.Update(settings, "mySolution1"); - [TestMethod] - public void Get_ErrorOccuredWhenWritingFile_ErrorIsLoggedAndReturnsNull() - { - var logger = new TestLogger(); + CheckFileWritten(settings, "mySolution1"); + logger.AssertNoOutputMessages(); + } - var file = new Mock(); - file.Setup(f => f.ReadAllText(GetFilePath("projectKey"))).Throws(new Exception("Test Exception")); - Mock fileSystem = CreateFileSystem(file); - RoslynSettingsFileStorage fileStorage = CreateTestSubject(logger, fileSystem.Object); + [TestMethod] + public void Get_HasNoIssues_ReturnsEmpty() + { + var settings = new RoslynSettings { SonarProjectKey = "projectKey", Suppressions = Enumerable.Empty() }; + MockFileReadAllText(settings); - var actual = fileStorage.Get("projectKey"); + 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(); - } + var issuesGotten = actual.Suppressions.ToList(); + file.Received(1).ReadAllText(GetFilePath("projectKey")); + logger.AssertNoOutputMessages(); + issuesGotten.Count.Should().Be(0); + } - [TestMethod] - public void Update_HasNoIssues_FileWritten() - { - var settings = new RoslynSettings - { - SonarProjectKey = "projectKey", - Suppressions = Enumerable.Empty() - }; + [TestMethod] + public void Get_FileDoesNotExist_ErrorIsLoggedAndReturnsNull() + { + MockFileSystem(false); - var logger = new TestLogger(); + var actual = testSubject.Get("projectKey"); - var file = new Mock(); - Mock fileSystem = CreateFileSystem(file); + 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(); + } - var fileStorage = CreateTestSubject(logger, fileSystem.Object); + [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); + } - fileStorage.Update(settings, "mySolution1"); + [TestMethod] + public void Delete_FileIsDeleted() + { + testSubject.Delete(SolutionName); - CheckFileWritten(file, settings, "mySolution1"); - logger.AssertNoOutputMessages(); - } + file.Received(1).Delete(GetFilePath(SolutionName)); + logger.AssertNoOutputMessages(); + } - [TestMethod] - public void Get_HasNoIssues_ReturnsEmpty() - { - var settings = new RoslynSettings - { - SonarProjectKey = "projectKey", - Suppressions = Enumerable.Empty() - }; + [TestMethod] + public void Delete_DeletionFails_Logs() + { + var errorMessage = "deletion failed"; + fileSystem.File.When(x => x.Delete(GetFilePath(SolutionName))).Do(_ => throw new Exception(errorMessage)); - var logger = new TestLogger(); + testSubject.Delete(SolutionName); - var file = CreateFileForGet(settings); - Mock fileSystem = CreateFileSystem(file); + file.Received(1).Delete(GetFilePath(SolutionName)); + logger.AssertPartialOutputStrings(string.Format(Strings.RoslynSettingsFileStorageDeleteError, SolutionName, errorMessage)); + } - var fileStorage = CreateTestSubject(logger, fileSystem.Object); + [TestMethod] + public void Delete_CriticalException_ExceptionThrown() + { + fileSystem.File.When(x => x.Delete(GetFilePath(SolutionName))).Do(_ => throw new StackOverflowException()); - var actual = fileStorage.Get("projectKey"); - var issuesGotten = actual.Suppressions.ToList(); + Action act = () => testSubject.Delete(SolutionName); - file.Verify(f => f.ReadAllText(GetFilePath("projectKey")), Times.Once); - logger.AssertNoOutputMessages(); + act.Should().Throw(); + } - issuesGotten.Count.Should().Be(0); - } + private void MockFileSystem(bool fileExists = true) + { + file.Exists(Arg.Any()).Returns(fileExists); + fileSystem.File.Returns(file); - [TestMethod] - public void Get_FileDoesNotExist_ErrorIsLoggedAndReturnsNull() - { - var logger = new TestLogger(); + var directoryObject = Substitute.For(); + fileSystem.Directory.Returns(directoryObject); + } - Mock fileSystem = CreateFileSystem(fileExists:false); - RoslynSettingsFileStorage fileStorage = CreateTestSubject(logger, fileSystem.Object); + private void CheckFileWritten(RoslynSettings settings, string solutionName) + { + var expectedFilePath = GetFilePath(solutionName); + var expectedContent = JsonConvert.SerializeObject(settings, Formatting.Indented); - var actual = fileStorage.Get("projectKey"); + file.Received(1).WriteAllText(expectedFilePath, expectedContent); + } - 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(); - } + private static string GetFilePath(string projectKey) => RoslynSettingsFileInfo.GetSettingsFilePath(projectKey); - [TestMethod] // Regression test for SLVS-2946 - public void SaveAndLoadSettings() - { - string serializedText = null; - var file = CreateSaveAndReloadFile(); - var fileSystem = CreateFileSystem(file); - var testSubject = CreateTestSubject(fileSystem: fileSystem.Object); + private void MockFileReadAllText(RoslynSettings settings) => file.ReadAllText(GetFilePath(settings.SonarProjectKey)).Returns(JsonConvert.SerializeObject(settings)); - var projectKey = "projectKey"; - var original = new RoslynSettings + private void CreateSaveAndReloadFile(string serializedText) + { + // "Save" the data that was written + file.When(x => x.WriteAllText(Arg.Any(), Arg.Any())) + .Do(callInfo => { - SonarProjectKey = projectKey, - Suppressions = new[] + 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( + _ => { - CreateIssue("rule1", "path1", null, "hash", RoslynLanguage.CSharp), // 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); - - Mock CreateSaveAndReloadFile() - { - // Create a file that that handles "saving" data and - // "reading" the saved file back again. - var file = new Mock(); - - // "Save" the data that was written - file.Setup(x => x.WriteAllText(It.IsAny(), It.IsAny())) - .Callback((_, contents) => { serializedText = contents; }); - - // "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.Setup(x => x.ReadAllText(It.IsAny())) - .Returns( - () => { - serializedText.Should().NotBeNull("Test error: data has not been saved"); - return serializedText; - }); - - return file; - } - } - - private RoslynSettingsFileStorage CreateTestSubject(ILogger logger = null, IFileSystem fileSystem = null) - { - logger ??= Mock.Of(); - fileSystem ??= CreateFileSystem().Object; - return new RoslynSettingsFileStorage(logger, fileSystem); - } - - private Mock CreateFileSystem(Mock file = null, bool fileExists = true) - { - file ??= new Mock(); - file.Setup(f => f.Exists(It.IsAny())).Returns(fileExists); - - var directoryObject = Mock.Of(); - var fileSystem = new Mock(); - - fileSystem.SetupGet(fs => fs.File).Returns(file.Object); - fileSystem.SetupGet(fs => fs.Directory).Returns(directoryObject); - - return fileSystem; - } - - private static void CheckFileWritten(Mock file, RoslynSettings settings, string solutionName) - { - var expectedFilePath = GetFilePath(solutionName); - var expectedContent = JsonConvert.SerializeObject(settings, Formatting.Indented); - - file.Verify(f => f.WriteAllText(expectedFilePath, expectedContent), Times.Once); - } - - private static string GetFilePath(string projectKey) => RoslynSettingsFileInfo.GetSettingsFilePath(projectKey); - - private Mock CreateFileForGet(RoslynSettings settings) - { - var file = new Mock(); - file.Setup(f => f.ReadAllText(GetFilePath(settings.SonarProjectKey))).Returns(JsonConvert.SerializeObject(settings)); - return file; - } - + serializedText.Should().NotBeNull("Test error: data has not been saved"); + return serializedText; + }); } } diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/RoslynSettingsFileSynchronizer.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/RoslynSettingsFileSynchronizer.cs index 3e335e03c1..064a65967c 100644 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/RoslynSettingsFileSynchronizer.cs +++ b/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/RoslynSettingsFileSynchronizer.cs @@ -18,11 +18,8 @@ * 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.Tasks; +using System.IO; using Microsoft.VisualStudio.Threading; using SonarLint.VisualStudio.ConnectedMode.Suppressions; using SonarLint.VisualStudio.Core; @@ -32,170 +29,175 @@ using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; using SonarQube.Client.Models; -namespace SonarLint.VisualStudio.Roslyn.Suppressions.InProcess +namespace SonarLint.VisualStudio.Roslyn.Suppressions.InProcess; + +/// +/// Responsible for listening to and calling +/// with the new suppressions. +/// +public interface IRoslynSettingsFileSynchronizer : IDisposable { - /// - /// Responsible for listening to and calling - /// with the new suppressions. - /// - public interface IRoslynSettingsFileSynchronizer : IDisposable + Task UpdateFileStorageAsync(); +} + +[Export(typeof(IRoslynSettingsFileSynchronizer))] +[PartCreationPolicy(CreationPolicy.NonShared)] // stateless - doesn't need to be shared +internal sealed class RoslynSettingsFileSynchronizer : IRoslynSettingsFileSynchronizer +{ + private readonly IConfigurationProvider configurationProvider; + private readonly ILogger logger; + private readonly IRoslynSettingsFileStorage roslynSettingsFileStorage; + private readonly IServerIssuesStore serverIssuesStore; + private readonly ISolutionInfoProvider solutionInfoProvider; + private readonly IThreadHandling threadHandling; + + [ImportingConstructor] + public RoslynSettingsFileSynchronizer( + IServerIssuesStore serverIssuesStore, + IRoslynSettingsFileStorage roslynSettingsFileStorage, + IConfigurationProvider configurationProvider, + ISolutionInfoProvider solutionInfoProvider, + ILogger logger) + : this(serverIssuesStore, + roslynSettingsFileStorage, + configurationProvider, + solutionInfoProvider, + logger, + ThreadHandling.Instance) { - Task UpdateFileStorageAsync(); } - [Export(typeof(IRoslynSettingsFileSynchronizer))] - [PartCreationPolicy(CreationPolicy.NonShared)] // stateless - doesn't need to be shared - internal sealed class RoslynSettingsFileSynchronizer : IRoslynSettingsFileSynchronizer + internal RoslynSettingsFileSynchronizer( + IServerIssuesStore serverIssuesStore, + IRoslynSettingsFileStorage roslynSettingsFileStorage, + IConfigurationProvider configurationProvider, + ISolutionInfoProvider solutionInfoProvider, + ILogger logger, + IThreadHandling threadHandling) { - private readonly IThreadHandling threadHandling; - private readonly IServerIssuesStore serverIssuesStore; - private readonly IRoslynSettingsFileStorage roslynSettingsFileStorage; - private readonly IConfigurationProvider configurationProvider; - private readonly ISolutionInfoProvider solutionInfoProvider; - private readonly ILogger logger; - - [ImportingConstructor] - public RoslynSettingsFileSynchronizer(IServerIssuesStore serverIssuesStore, - IRoslynSettingsFileStorage roslynSettingsFileStorage, - IConfigurationProvider configurationProvider, - ISolutionInfoProvider solutionInfoProvider, - ILogger logger) - : this(serverIssuesStore, - roslynSettingsFileStorage, - configurationProvider, - solutionInfoProvider, - logger, - ThreadHandling.Instance) - { - } + this.serverIssuesStore = serverIssuesStore; + this.roslynSettingsFileStorage = roslynSettingsFileStorage; + this.configurationProvider = configurationProvider; + this.solutionInfoProvider = solutionInfoProvider; + this.logger = logger; + this.threadHandling = threadHandling; + + serverIssuesStore.ServerIssuesChanged += OnServerIssuesChanged; + } - internal RoslynSettingsFileSynchronizer(IServerIssuesStore serverIssuesStore, - IRoslynSettingsFileStorage roslynSettingsFileStorage, - IConfigurationProvider configurationProvider, - ISolutionInfoProvider solutionInfoProvider, - ILogger logger, - IThreadHandling threadHandling) + /// + /// Updates the Roslyn suppressed issues file if in connected mode + /// + /// The method will switch to a background if required, and will *not* + /// return to the UI thread on completion. + public async Task UpdateFileStorageAsync() + { + CodeMarkers.Instance.FileSynchronizerUpdateStart(); + try { - this.serverIssuesStore = serverIssuesStore; - this.roslynSettingsFileStorage = roslynSettingsFileStorage; - this.configurationProvider = configurationProvider; - this.solutionInfoProvider = solutionInfoProvider; - this.logger = logger; - this.threadHandling = threadHandling; - - serverIssuesStore.ServerIssuesChanged += OnServerIssuesChanged; - } + await threadHandling.SwitchToBackgroundThread(); - private void OnServerIssuesChanged(object sender, EventArgs e) - { - // Called on the UI thread, so unhandled exceptions will crash VS. - // Note: we don't expect any exceptions to be thrown, since the called method - // does all of its work on a background thread. - try - { - UpdateFileStorageAsync().Forget(); - } - catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) - { - // Squash non-critical exceptions - logger.LogVerbose(ex.ToString()); - } - } + var sonarProjectKey = configurationProvider.GetConfiguration().Project?.ServerProjectKey; - /// - /// Updates the Roslyn suppressed issues file if in connected mode - /// - /// The method will switch to a background if required, and will *not* - /// return to the UI thread on completion. - public async Task UpdateFileStorageAsync() - { - CodeMarkers.Instance.FileSynchronizerUpdateStart(); - try + var solutionNameWithoutExtension = await GetSolutionNameWithoutExtensionAsync(); + if (!string.IsNullOrEmpty(sonarProjectKey)) { - await threadHandling.SwitchToBackgroundThread(); - - var sonarProjectKey = configurationProvider.GetConfiguration().Project?.ServerProjectKey; - - if (!string.IsNullOrEmpty(sonarProjectKey)) + var allSuppressedIssues = serverIssuesStore.Get(); + var settings = new RoslynSettings { - var fullSolutionFilePath = await solutionInfoProvider.GetFullSolutionFilePathAsync(); - Debug.Assert(fullSolutionFilePath != null, "Not expecting the solution name to be null in Connected Mode"); - var solnNameWithoutExtension = System.IO.Path.GetFileNameWithoutExtension(fullSolutionFilePath); - - var allSuppressedIssues = serverIssuesStore.Get(); - - var settings = new RoslynSettings - { - SonarProjectKey = sonarProjectKey, - Suppressions = allSuppressedIssues - .Where(x => x.IsResolved) - .Select(x => IssueConverter.Convert(x)) - .Where(x => x.RoslynLanguage != RoslynLanguage.Unknown && !string.IsNullOrEmpty(x.RoslynRuleId)) - .ToArray(), - }; - roslynSettingsFileStorage.Update(settings, solnNameWithoutExtension); - } + SonarProjectKey = sonarProjectKey, + Suppressions = allSuppressedIssues + .Where(x => x.IsResolved) + .Select(x => IssueConverter.Convert(x)) + .Where(x => x.RoslynLanguage != RoslynLanguage.Unknown && !string.IsNullOrEmpty(x.RoslynRuleId)) + .ToArray() + }; + roslynSettingsFileStorage.Update(settings, solutionNameWithoutExtension); } - finally + else { - CodeMarkers.Instance.FileSynchronizerUpdateStop(); + roslynSettingsFileStorage.Delete(solutionNameWithoutExtension); } } - - public void Dispose() + finally { - serverIssuesStore.ServerIssuesChanged -= OnServerIssuesChanged; + CodeMarkers.Instance.FileSynchronizerUpdateStop(); } + } - // Converts SonarQube issues to SuppressedIssues that can be compared more easily with Roslyn issues - internal static class IssueConverter + public void Dispose() => serverIssuesStore.ServerIssuesChanged -= OnServerIssuesChanged; + + private void OnServerIssuesChanged(object sender, EventArgs e) + { + // Called on the UI thread, so unhandled exceptions will crash VS. + // Note: we don't expect any exceptions to be thrown, since the called method + // does all of its work on a background thread. + try { - public static SuppressedIssue Convert(SonarQubeIssue issue) - { - (string repoKey, string ruleKey) = GetRepoAndRuleKey(issue.RuleId); - var language = GetRoslynLanguage(repoKey); + UpdateFileStorageAsync().Forget(); + } + catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) + { + // Squash non-critical exceptions + logger.LogVerbose(ex.ToString()); + } + } - int? 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 - }; - } + private async Task GetSolutionNameWithoutExtensionAsync() + { + var fullSolutionFilePath = await solutionInfoProvider.GetFullSolutionFilePathAsync(); + Debug.Assert(fullSolutionFilePath != null, "Not expecting the solution name to be null in Connected Mode"); + return Path.GetFileNameWithoutExtension(fullSolutionFilePath); + } - private static (string repoKey, string ruleKey) GetRepoAndRuleKey(string sonarRuleId) + // 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 { - // 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); + RoslynRuleId = ruleKey, + FilePath = issue.FilePath, + Hash = issue.Hash, + RoslynLanguage = language, + RoslynIssueLine = line + }; + } - return (repoKey, ruleKey); - } + 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 (null, null); // invalid rule key -> ignore + return (repoKey, ruleKey); } - private static RoslynLanguage GetRoslynLanguage(string repoKey) + 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) { - // 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; - } + 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/Resources/Strings.Designer.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/Resources/Strings.Designer.cs index 0e9387b124..2f504f781a 100644 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/Resources/Strings.Designer.cs +++ b/src/Roslyn.Suppressions/Roslyn.Suppressions/Resources/Strings.Designer.cs @@ -69,6 +69,15 @@ internal static string FileWatcherException { } } + /// + /// 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. /// diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/Resources/Strings.resx b/src/Roslyn.Suppressions/Roslyn.Suppressions/Resources/Strings.resx index 7a8d16966b..53fb1b9897 100644 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/Resources/Strings.resx +++ b/src/Roslyn.Suppressions/Roslyn.Suppressions/Resources/Strings.resx @@ -120,6 +120,9 @@ [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 diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/SettingsFile/RoslynSettingsFileStorage.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/SettingsFile/RoslynSettingsFileStorage.cs index b03e3eb359..f90eb33efe 100644 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/SettingsFile/RoslynSettingsFileStorage.cs +++ b/src/Roslyn.Suppressions/Roslyn.Suppressions/SettingsFile/RoslynSettingsFileStorage.cs @@ -18,99 +18,119 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using System.ComponentModel.Composition; -using System.Diagnostics; 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 +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 { - internal interface IRoslynSettingsFileStorage + private readonly IFileSystem fileSystem; + private readonly ILogger logger; + + [ImportingConstructor] + public RoslynSettingsFileStorage(ILogger logger) : this(logger, new FileSystem()) { - /// - /// 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); + internal RoslynSettingsFileStorage(ILogger logger, IFileSystem fileSystem) + { + this.fileSystem = fileSystem; + this.logger = logger; + fileSystem.Directory.CreateDirectory(RoslynSettingsFileInfo.Directory); } - [Export(typeof(IRoslynSettingsFileStorage))] - internal class RoslynSettingsFileStorage : IRoslynSettingsFileStorage + public RoslynSettings Get(string settingsKey) { - private readonly ILogger logger; - private readonly IFileSystem fileSystem; + Debug.Assert(settingsKey != null, "Not expecting settings to be null"); - [ImportingConstructor] - public RoslynSettingsFileStorage(ILogger logger) : this(logger, new FileSystem()) + try { - } + CodeMarkers.Instance.FileStorageGetStart(); + var filePath = RoslynSettingsFileInfo.GetSettingsFilePath(settingsKey); - internal RoslynSettingsFileStorage(ILogger logger, IFileSystem fileSystem) + 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 { - this.fileSystem = fileSystem; - this.logger = logger; - fileSystem.Directory.CreateDirectory(RoslynSettingsFileInfo.Directory); + CodeMarkers.Instance.FileStorageGetStop(); } + return null; + } - public RoslynSettings Get(string settingsKey) + public void Delete(string solutionNameWithoutExtension) + { + try { - Debug.Assert(settingsKey != null, "Not expecting settings to be null"); - - try - { - CodeMarkers.Instance.FileStorageGetStart(); - var filePath = RoslynSettingsFileInfo.GetSettingsFilePath(settingsKey); + 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(); + } + } - if(!fileSystem.File.Exists(filePath)) - { - logger.WriteLine(string.Format(Strings.RoslynSettingsFileStorageGetError, settingsKey, Strings.RoslynSettingsFileStorageFileNotFound)); - return null; - } + 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"); - 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; + try + { + CodeMarkers.Instance.FileStorageUpdateStart(); + var filePath = RoslynSettingsFileInfo.GetSettingsFilePath(solutionNameWithoutExtension); + var fileContent = JsonConvert.SerializeObject(settings, Formatting.Indented); + fileSystem.File.WriteAllText(filePath, fileContent); } - - public void Update(RoslynSettings settings, string solutionNameWithoutExtension) + catch (Exception ex) { - 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(); - } + logger.WriteLine(string.Format(Strings.RoslynSettingsFileStorageUpdateError, settings.SonarProjectKey, ex.Message)); + } + finally + { + CodeMarkers.Instance.FileStorageUpdateStop(); } } }