From 9ba426cb8ffb7197ec194594ba21307448b0dc66 Mon Sep 17 00:00:00 2001 From: Gabriela Trutan Date: Fri, 15 Nov 2024 14:04:48 +0100 Subject: [PATCH] SLVS-1625 CaYC: Replace Moq with Nsubstitute (#5836) --- ...t.VisualStudio.Integration.sln.DotSettings | 7 +- .../UI/ProgressReporterViewModelTests.cs | 2 +- src/Education.UnitTests/EducationTests.cs | 282 ++++--- ...narErrorListEventProcessorProviderTests.cs | 40 +- .../SonarErrorListEventProcessorTests.cs | 142 ++-- .../Rule/SLCoreRuleMetaDataProviderTests.cs | 273 +++---- src/Education/Education.cs | 145 ++-- .../ErrorList/SonarErrorListEventProcessor.cs | 59 +- .../SonarErrorListEventProcessorProvider.cs | 61 +- .../Rule/SLCoreRuleMetaDataProvider.cs | 25 +- .../ErrorListHelperTests.cs | 738 ++++++++---------- src/Infrastructure.VS/ErrorListHelper.cs | 288 ++++--- .../Models/AnalysisIssueVisualizationTests.cs | 342 ++++---- .../Models/AnalysisIssueVisualization.cs | 178 ++--- 14 files changed, 1175 insertions(+), 1407 deletions(-) diff --git a/SonarLint.VisualStudio.Integration.sln.DotSettings b/SonarLint.VisualStudio.Integration.sln.DotSettings index 5f03c02e18..cc86674657 100644 --- a/SonarLint.VisualStudio.Integration.sln.DotSettings +++ b/SonarLint.VisualStudio.Integration.sln.DotSettings @@ -132,4 +132,9 @@ False False True - False \ No newline at end of file + False + True + True + True + True + True \ No newline at end of file diff --git a/src/ConnectedMode.UnitTests/UI/ProgressReporterViewModelTests.cs b/src/ConnectedMode.UnitTests/UI/ProgressReporterViewModelTests.cs index a815f1ea80..8e36c7a406 100644 --- a/src/ConnectedMode.UnitTests/UI/ProgressReporterViewModelTests.cs +++ b/src/ConnectedMode.UnitTests/UI/ProgressReporterViewModelTests.cs @@ -27,8 +27,8 @@ namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.UI; [TestClass] public class ProgressReporterViewModelTests { - private ProgressReporterViewModel testSubject; private ILogger logger; + private ProgressReporterViewModel testSubject; [TestInitialize] public void TestInitialize() diff --git a/src/Education.UnitTests/EducationTests.cs b/src/Education.UnitTests/EducationTests.cs index c049485d9e..d7a452d2cd 100644 --- a/src/Education.UnitTests/EducationTests.cs +++ b/src/Education.UnitTests/EducationTests.cs @@ -19,163 +19,145 @@ */ using System.Windows.Documents; -using Moq; +using NSubstitute.ReturnsExtensions; using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Suppressions; using SonarLint.VisualStudio.Education.Rule; using SonarLint.VisualStudio.Education.XamlGenerator; using SonarLint.VisualStudio.TestInfrastructure; -namespace SonarLint.VisualStudio.Education.UnitTests +namespace SonarLint.VisualStudio.Education.UnitTests; + +[TestClass] +public class EducationTests { - [TestClass] - public class EducationTests + private readonly SonarCompositeRuleId knownRule = new("repoKey", "ruleKey"); + private readonly SonarCompositeRuleId unknownRule = new("known", "xxx"); + + private ILogger logger; + private IRuleHelpToolWindow ruleDescriptionToolWindow; + private IRuleHelpXamlBuilder ruleHelpXamlBuilder; + private IRuleInfo ruleInfo; + private IRuleMetaDataProvider ruleMetadataProvider; + private IShowRuleInBrowser showRuleInBrowser; + private Education testSubject; + private IThreadHandling threadHandling; + private IToolWindowService toolWindowService; + + [TestInitialize] + public void TestInitialize() + { + toolWindowService = Substitute.For(); + ruleMetadataProvider = Substitute.For(); + showRuleInBrowser = Substitute.For(); + ruleHelpXamlBuilder = Substitute.For(); + ruleDescriptionToolWindow = Substitute.For(); + ruleInfo = Substitute.For(); + logger = new TestLogger(true); + threadHandling = new NoOpThreadHandler(); + SetupKnownRule(); + SetupUnknownRule(); + + testSubject = new Education(toolWindowService, ruleMetadataProvider, showRuleInBrowser, logger, ruleHelpXamlBuilder, threadHandling); + } + + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + + [TestMethod] + public void Ctor_IsFreeThreaded() + { + toolWindowService.ReceivedCalls().Should().HaveCount(0); + ruleMetadataProvider.ReceivedCalls().Should().HaveCount(0); + showRuleInBrowser.ReceivedCalls().Should().HaveCount(0); + ruleHelpXamlBuilder.ReceivedCalls().Should().HaveCount(0); + } + + [TestMethod] + public void ShowRuleHelp_KnownRule_DocumentIsDisplayedInToolWindow() + { + var flowDocument = MockFlowDocument(); + toolWindowService.GetToolWindow().Returns(ruleDescriptionToolWindow); + + testSubject.ShowRuleHelp(knownRule, null, null); + + VerifyGetsRuleInfoForCorrectRuleId(knownRule); + VerifyRuleIsDisplayedInIde(flowDocument); + VerifyRuleNotShownInBrowser(); + } + + [TestMethod] + public void ShowRuleHelp_FailedToDisplayRule_RuleIsShownInBrowser() + { + ruleHelpXamlBuilder.When(x => x.Create(ruleInfo, /* todo by SLVS-1630 */ null)).Do(x => throw new Exception("some layout error")); + + testSubject.ShowRuleHelp(knownRule, null, /* todo by SLVS-1630 */ null); + + VerifyGetsRuleInfoForCorrectRuleId(knownRule); + VerifyRuleShownInBrowser(knownRule); + VerifyAttemptsToBuildRuleButFails(); + } + + [TestMethod] + public void ShowRuleHelp_UnknownRule_RuleIsShownInBrowser() + { + testSubject.ShowRuleHelp(unknownRule, null, /* todo by SLVS-1630 */ null); + + VerifyGetsRuleInfoForCorrectRuleId(unknownRule); + VerifyRuleShownInBrowser(unknownRule); + VerifyNotAttemptsBuildRule(); + } + + [TestMethod] + public void ShowRuleHelp_FilterableIssueProvided_CallsGetRuleInfoForIssue() + { + var issueId = Guid.NewGuid(); + + testSubject.ShowRuleHelp(knownRule, issueId, null); + + ruleMetadataProvider.Received(1).GetRuleInfoAsync(knownRule, issueId); + } + + private void VerifyGetsRuleInfoForCorrectRuleId(SonarCompositeRuleId ruleId) => ruleMetadataProvider.Received(1).GetRuleInfoAsync(ruleId, Arg.Any()); + + private void VerifyRuleShownInBrowser(SonarCompositeRuleId ruleId) => showRuleInBrowser.Received(1).ShowRuleDescription(ruleId); + + private void VerifyRuleNotShownInBrowser() => showRuleInBrowser.ReceivedCalls().Should().HaveCount(0); + + private void VerifyToolWindowShown() => toolWindowService.Received(1).Show(RuleHelpToolWindow.ToolWindowId); + + private void VerifyAttemptsToBuildRuleButFails() { - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } + ruleHelpXamlBuilder.ReceivedCalls().Should().HaveCount(1); + toolWindowService.ReceivedCalls().Should().HaveCount(1); + } + + private void VerifyNotAttemptsBuildRule() + { + ruleHelpXamlBuilder.ReceivedCalls().Should().HaveCount(0); + toolWindowService.ReceivedCalls().Should().HaveCount(0); + } + + private void VerifyRuleIsDisplayedInIde(FlowDocument flowDocument) + { + ruleHelpXamlBuilder.Received(1).Create(ruleInfo, /* todo by SLVS-1630 */ null); + ruleDescriptionToolWindow.Received(1).UpdateContent(flowDocument); + VerifyToolWindowShown(); + } + + private void SetupKnownRule() => ruleMetadataProvider.GetRuleInfoAsync(knownRule, Arg.Any()).Returns(ruleInfo); - [TestMethod] - public void ShowRuleHelp_KnownRule_DocumentIsDisplayedInToolWindow() - { - var ruleMetaDataProvider = new Mock(); - var ruleId = new SonarCompositeRuleId("repoKey", "ruleKey"); - - var ruleInfo = Mock.Of(); - ruleMetaDataProvider.Setup(x => x.GetRuleInfoAsync(It.IsAny(), It.IsAny())).ReturnsAsync(ruleInfo); - - var flowDocument = Mock.Of(); - var ruleHelpXamlBuilder = new Mock(); - ruleHelpXamlBuilder.Setup(x => x.Create(ruleInfo, /* todo by SLVS-1630 */ null)).Returns(flowDocument); - - var ruleDescriptionToolWindow = new Mock(); - - var toolWindowService = new Mock(); - toolWindowService.Setup(x => x.GetToolWindow()).Returns(ruleDescriptionToolWindow.Object); - - var showRuleInBrowser = new Mock(); - var testSubject = CreateEducation(toolWindowService.Object, - ruleMetaDataProvider.Object, - showRuleInBrowser.Object, - ruleHelpXamlBuilder.Object); - - // Sanity check - tool window not yet fetched - toolWindowService.Invocations.Should().HaveCount(0); - - // Act - testSubject.ShowRuleHelp(ruleId, null, null); - - ruleMetaDataProvider.Verify(x => x.GetRuleInfoAsync(ruleId, It.IsAny()), Times.Once); - ruleHelpXamlBuilder.Verify(x => x.Create(ruleInfo, /* todo by SLVS-1630 */ null), Times.Once); - ruleDescriptionToolWindow.Verify(x => x.UpdateContent(flowDocument), Times.Once); - toolWindowService.Verify(x => x.Show(RuleHelpToolWindow.ToolWindowId), Times.Once); - - showRuleInBrowser.Invocations.Should().HaveCount(0); - } - - [TestMethod] - public void ShowRuleHelp_FailedToDisplayRule_RuleIsShownInBrowser() - { - var toolWindowService = new Mock(); - var ruleMetadataProvider = new Mock(); - var ruleHelpXamlBuilder = new Mock(); - var showRuleInBrowser = new Mock(); - - var ruleId = new SonarCompositeRuleId("repoKey", "ruleKey"); - - var ruleInfo = Mock.Of(); - ruleMetadataProvider.Setup(x => x.GetRuleInfoAsync(It.IsAny(), It.IsAny())).ReturnsAsync(ruleInfo); - - ruleHelpXamlBuilder.Setup(x => x.Create(ruleInfo, /* todo by SLVS-1630 */ null)).Throws(new Exception("some layout error")); - - var testSubject = CreateEducation( - toolWindowService.Object, - ruleMetadataProvider.Object, - showRuleInBrowser.Object, - ruleHelpXamlBuilder.Object); - - toolWindowService.Reset(); // Called in the constructor, so need to reset to clear the list of invocations - - testSubject.ShowRuleHelp(ruleId, null, /* todo by SLVS-1630 */null); - - ruleMetadataProvider.Verify(x => x.GetRuleInfoAsync(ruleId, It.IsAny()), Times.Once); - showRuleInBrowser.Verify(x => x.ShowRuleDescription(ruleId), Times.Once); - - // should have attempted to build the rule, but failed - ruleHelpXamlBuilder.Invocations.Should().HaveCount(1); - toolWindowService.Invocations.Should().HaveCount(1); - } - - [TestMethod] - public void ShowRuleHelp_UnknownRule_RuleIsShownInBrowser() - { - var toolWindowService = new Mock(); - var ruleMetadataProvider = new Mock(); - var ruleHelpXamlBuilder = new Mock(); - var showRuleInBrowser = new Mock(); - - var unknownRule = new SonarCompositeRuleId("known", "xxx"); - ruleMetadataProvider.Setup(x => x.GetRuleInfoAsync(unknownRule, It.IsAny())).ReturnsAsync((IRuleInfo)null); - - var testSubject = CreateEducation( - toolWindowService.Object, - ruleMetadataProvider.Object, - showRuleInBrowser.Object, - ruleHelpXamlBuilder.Object); - - toolWindowService.Reset(); // Called in the constructor, so need to reset to clear the list of invocations - - testSubject.ShowRuleHelp(unknownRule, null, /* todo by SLVS-1630 */ null); - - ruleMetadataProvider.Verify(x => x.GetRuleInfoAsync(unknownRule, It.IsAny()), Times.Once); - showRuleInBrowser.Verify(x => x.ShowRuleDescription(unknownRule), Times.Once); - - // Should not have attempted to build the rule - ruleHelpXamlBuilder.Invocations.Should().HaveCount(0); - toolWindowService.Invocations.Should().HaveCount(0); - } - - [TestMethod] - public void ShowRuleHelp_FilterableIssueProvided_CallsGetRuleInfoForIssue() - { - var toolWindowService = new Mock(); - var ruleMetadataProvider = new Mock(); - var ruleHelpXamlBuilder = new Mock(); - var showRuleInBrowser = new Mock(); - var issueId = Guid.NewGuid(); - var ruleId = new SonarCompositeRuleId("repoKey", "ruleKey"); - ruleMetadataProvider.Setup(x => x.GetRuleInfoAsync(ruleId, issueId)).ReturnsAsync((IRuleInfo)null); - var testSubject = CreateEducation( - toolWindowService.Object, - ruleMetadataProvider.Object, - showRuleInBrowser.Object, - ruleHelpXamlBuilder.Object); - - testSubject.ShowRuleHelp(ruleId,issueId, null); - - ruleMetadataProvider.Verify(x => x.GetRuleInfoAsync(ruleId, issueId), Times.Once); - } - - private Education CreateEducation(IToolWindowService toolWindowService = null, - IRuleMetaDataProvider ruleMetadataProvider = null, - IShowRuleInBrowser showRuleInBrowser = null, - IRuleHelpXamlBuilder ruleHelpXamlBuilder = null) - { - toolWindowService ??= Mock.Of(); - ruleMetadataProvider ??= Mock.Of(); - showRuleInBrowser ??= Mock.Of(); - ruleHelpXamlBuilder ??= Mock.Of(); - var logger = new TestLogger(logToConsole: true); - var threadHandling = new NoOpThreadHandler(); - - return new Education(toolWindowService, ruleMetadataProvider, showRuleInBrowser, logger, ruleHelpXamlBuilder, threadHandling); - } + private void SetupUnknownRule() => ruleMetadataProvider.GetRuleInfoAsync(unknownRule, Arg.Any()).ReturnsNull(); + + private FlowDocument MockFlowDocument() + { + var flowDocument = Substitute.For(); + ruleHelpXamlBuilder.Create(ruleInfo, /* todo by SLVS-1630 */ null).Returns(flowDocument); + return flowDocument; } } diff --git a/src/Education.UnitTests/ErrorList/SonarErrorListEventProcessorProviderTests.cs b/src/Education.UnitTests/ErrorList/SonarErrorListEventProcessorProviderTests.cs index e6a761f543..75b6199b45 100644 --- a/src/Education.UnitTests/ErrorList/SonarErrorListEventProcessorProviderTests.cs +++ b/src/Education.UnitTests/ErrorList/SonarErrorListEventProcessorProviderTests.cs @@ -18,37 +18,31 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using FluentAssertions; using Microsoft.VisualStudio.Shell.TableControl; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Education.SonarLint.VisualStudio.Education.ErrorList; +using SonarLint.VisualStudio.Education.ErrorList; using SonarLint.VisualStudio.Infrastructure.VS; using SonarLint.VisualStudio.TestInfrastructure; -namespace SonarLint.VisualStudio.Education.UnitTests.ErrorList +namespace SonarLint.VisualStudio.Education.UnitTests.ErrorList; + +[TestClass] +public class SonarErrorListEventProcessorProviderTests { - [TestClass] - public class SonarErrorListEventProcessorProviderTests - { - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); - [TestMethod] - public void Get_CreatesAndReturnsProcessor() - { - var testSubject = new SonarErrorListEventProcessorProvider(Mock.Of(), Mock.Of(), Mock.Of()); + [TestMethod] + public void Get_CreatesAndReturnsProcessor() + { + var testSubject = new SonarErrorListEventProcessorProvider(Substitute.For(), Substitute.For(), Substitute.For()); - var actual = testSubject.GetAssociatedEventProcessor(Mock.Of()); + var actual = testSubject.GetAssociatedEventProcessor(Substitute.For()); - actual.Should().NotBeNull(); - } + actual.Should().NotBeNull(); } } diff --git a/src/Education.UnitTests/ErrorList/SonarErrorListEventProcessorTests.cs b/src/Education.UnitTests/ErrorList/SonarErrorListEventProcessorTests.cs index 8ce2c980d8..0ed066c36c 100644 --- a/src/Education.UnitTests/ErrorList/SonarErrorListEventProcessorTests.cs +++ b/src/Education.UnitTests/ErrorList/SonarErrorListEventProcessorTests.cs @@ -19,95 +19,97 @@ */ using Microsoft.VisualStudio.Shell.TableControl; -using Moq; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Suppressions; -using SonarLint.VisualStudio.Education.SonarLint.VisualStudio.Education.ErrorList; +using SonarLint.VisualStudio.Education.ErrorList; using SonarLint.VisualStudio.Infrastructure.VS; using SonarLint.VisualStudio.TestInfrastructure; -namespace SonarLint.VisualStudio.Education.UnitTests.ErrorList +namespace SonarLint.VisualStudio.Education.UnitTests.ErrorList; + +[TestClass] +public class SonarErrorListEventProcessorTests { - [TestClass] - public class SonarErrorListEventProcessorTests + private readonly TableEntryEventArgs eventArgs = new(); + + private IEducation education; + private IErrorListHelper errorListHelper; + private IFilterableIssue filterableIssue; + private ITableEntryHandle handle; + private ILogger logger; + private SonarErrorListEventProcessor testSubject; + + [TestInitialize] + public void TestInitialize() { - [TestMethod] - public void PreprocessNavigateToHelp_NotASonarRule_EventIsNotHandled() - { - SonarCompositeRuleId ruleId = null; - var handle = Mock.Of(); - var errorListHelper = CreateErrorListHelper(isSonarRule: false, ruleId); + education = Substitute.For(); + errorListHelper = Substitute.For(); + handle = Substitute.For(); + filterableIssue = Substitute.For(); + logger = new TestLogger(); - var education = new Mock(); - var eventArgs = new TableEntryEventArgs(); + testSubject = new SonarErrorListEventProcessor(education, errorListHelper, logger); + } - var testSubject = CreateTestSubject(education.Object, errorListHelper.Object); + [TestMethod] + public void PreprocessNavigateToHelp_NotASonarRule_EventIsNotHandled() + { + SonarCompositeRuleId ruleId = null; + MockErrorListHelper(false, ruleId); - testSubject.PreprocessNavigateToHelp(handle, eventArgs); + testSubject.PreprocessNavigateToHelp(handle, eventArgs); - errorListHelper.Verify(x => x.TryGetRuleId(handle, out ruleId)); - education.Invocations.Should().HaveCount(0); - eventArgs.Handled.Should().BeFalse(); - } + errorListHelper.Received(1).TryGetRuleId(handle, out _); + education.ReceivedCalls().Should().HaveCount(0); + eventArgs.Handled.Should().BeFalse(); + } - [TestMethod] - public void PreprocessNavigateToHelp_IsASonarRule_EventIsHandledAndEducationServiceCalled() - { - SonarCompositeRuleId ruleId; - SonarCompositeRuleId.TryParse("cpp:S123", out ruleId); - var handle = Mock.Of(); - var errorListHelper = CreateErrorListHelper(isSonarRule: true, ruleId); + [TestMethod] + public void PreprocessNavigateToHelp_IsASonarRule_EventIsHandledAndEducationServiceCalled() + { + var ruleId = CreateSonarCompositeRuleId("cpp:S123"); + MockErrorListHelper(true, ruleId); - var education = new Mock(); - var eventArgs = new TableEntryEventArgs(); + testSubject.PreprocessNavigateToHelp(handle, eventArgs); - var testSubject = CreateTestSubject(education.Object, errorListHelper.Object); + errorListHelper.Received(1).TryGetRuleId(handle, out _); + education.ReceivedCalls().Should().HaveCount(1); + education.Received(1).ShowRuleHelp(ruleId, null, /* todo by SLVS-1630 */ null); + eventArgs.Handled.Should().BeTrue(); + } - testSubject.PreprocessNavigateToHelp(handle, eventArgs); + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void PreprocessNavigateToHelp_IsASonarRule_EducationServiceIsCalledWithIssueId(bool getFilterableIssueResult) + { + var ruleId = CreateSonarCompositeRuleId("cpp:S123"); + MockErrorListHelper(true, ruleId); + MockGetFilterableIssue(getFilterableIssueResult); - errorListHelper.Verify(x => x.TryGetRuleId(handle, out ruleId)); - education.Invocations.Should().HaveCount(1); - education.Verify(x => x.ShowRuleHelp(ruleId, null, /* todo by SLVS-1630 */ null)); - eventArgs.Handled.Should().BeTrue(); - } + testSubject.PreprocessNavigateToHelp(handle, new TableEntryEventArgs()); - [TestMethod] - [DataRow(true)] - [DataRow(false)] - public void PreprocessNavigateToHelp_IsASonarRule_EducationServiceIsCalledWithIssueId(bool getFilterableIssueResult) - { - SonarCompositeRuleId ruleId; - IFilterableIssue filterableIssue = new Mock().Object; - SonarCompositeRuleId.TryParse("cpp:S123", out ruleId); - var handle = Mock.Of(); - var errorListHelper = CreateErrorListHelper(isSonarRule: true, ruleId); - errorListHelper.Setup(x => x.TryGetFilterableIssue(It.IsAny(), out filterableIssue)).Returns(getFilterableIssueResult); - var education = new Mock(); - var testSubject = CreateTestSubject(education.Object, errorListHelper.Object); - - testSubject.PreprocessNavigateToHelp(handle, new TableEntryEventArgs()); - - education.Invocations.Should().HaveCount(1); - education.Verify(x => x.ShowRuleHelp(ruleId, filterableIssue.IssueId, null)); - } - - private static Mock CreateErrorListHelper(bool isSonarRule, SonarCompositeRuleId ruleId) + education.ReceivedCalls().Should().HaveCount(1); + education.Received(1).ShowRuleHelp(ruleId, filterableIssue.IssueId, null); + } + + private void MockGetFilterableIssue(bool getFilterableIssueResult) => + errorListHelper.TryGetFilterableIssue(Arg.Any(), out _).Returns(callInfo => { - var mock = new Mock(); - mock.Setup(x => x.TryGetRuleId(It.IsAny(), out ruleId)).Returns(isSonarRule); - return mock; - } - - private static SonarErrorListEventProcessor CreateTestSubject(IEducation educationService = null, - IErrorListHelper errorListHelper = null, - ILogger logger = null) + callInfo[1] = filterableIssue; + return getFilterableIssueResult; + }); + + private void MockErrorListHelper(bool isSonarRule, SonarCompositeRuleId ruleId) => + errorListHelper.TryGetRuleId(Arg.Any(), out _).Returns(callInfo => { - educationService ??= Mock.Of(); - errorListHelper ??= Mock.Of(); - logger ??= new TestLogger(logToConsole: true); + callInfo[1] = ruleId; + return isSonarRule; + }); - var testSubject = new SonarErrorListEventProcessor(educationService, errorListHelper, logger); - return testSubject; - } + private static SonarCompositeRuleId CreateSonarCompositeRuleId(string errorListErrorCode) + { + SonarCompositeRuleId.TryParse(errorListErrorCode, out var ruleId); + return ruleId; } } diff --git a/src/Education.UnitTests/Rule/SLCoreRuleMetaDataProviderTests.cs b/src/Education.UnitTests/Rule/SLCoreRuleMetaDataProviderTests.cs index 676b139045..8e770ee2a6 100644 --- a/src/Education.UnitTests/Rule/SLCoreRuleMetaDataProviderTests.cs +++ b/src/Education.UnitTests/Rule/SLCoreRuleMetaDataProviderTests.cs @@ -18,24 +18,50 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using Moq; +using NSubstitute.ExceptionExtensions; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Education.Rule; using SonarLint.VisualStudio.SLCore.Core; -using SonarLint.VisualStudio.SLCore.Protocol; +using SonarLint.VisualStudio.SLCore.Service.Issue; +using SonarLint.VisualStudio.SLCore.Service.Issue.Models; using SonarLint.VisualStudio.SLCore.Service.Rules; using SonarLint.VisualStudio.SLCore.Service.Rules.Models; using SonarLint.VisualStudio.SLCore.State; using SonarLint.VisualStudio.TestInfrastructure; -using SonarLint.VisualStudio.SLCore.Service.Issue; -using SonarLint.VisualStudio.SLCore.Service.Issue.Models; namespace SonarLint.VisualStudio.Education.UnitTests.Rule; [TestClass] public class SLCoreRuleMetaDataProviderTests { - private static readonly SonarCompositeRuleId CompositeRuleId = new("rule", "key1"); + private readonly SonarCompositeRuleId compositeRuleId = new("rule", "key1"); + private readonly ConfigurationScope configurationScope = new("id"); + private readonly RuleInfo defaultRuleInfo = new(default, default, default, default, default, default, default, default); + private readonly EffectiveIssueDetailsDto effectiveIssueDetailsDto = new(default, default, default, default, default, default, default, default); + private readonly string errorMessage = "my message"; + private readonly Guid issueId = Guid.NewGuid(); + + private IActiveConfigScopeTracker configScopeTrackerMock; + private IIssueSLCoreService issueServiceMock; + private TestLogger logger; + private IRuleInfoConverter ruleInfoConverter; + private IRulesSLCoreService rulesServiceMock; + private ISLCoreServiceProvider serviceProviderMock; + private SLCoreRuleMetaDataProvider testSubject; + + [TestInitialize] + public void TestInitialize() + { + serviceProviderMock = Substitute.For(); + configScopeTrackerMock = Substitute.For(); + issueServiceMock = Substitute.For(); + rulesServiceMock = Substitute.For(); + ruleInfoConverter = Substitute.For(); + logger = new TestLogger(); + + testSubject = new SLCoreRuleMetaDataProvider(serviceProviderMock, configScopeTrackerMock, ruleInfoConverter, logger); + MockupServices(); + } [TestMethod] public void MefCtor_CheckIsExported() => @@ -51,12 +77,9 @@ public void MefCtor_CheckIsExported() => [TestMethod] public async Task GetRuleInfoAsync_NoActiveScope_ReturnsNull() { - var testSubject = - CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out var logger); - SetUpServiceProvider(serviceProviderMock, out _); - SetUpConfigScopeTracker(configScopeTrackerMock, null); + SetUpConfigScopeTracker(null); - var ruleInfo = await testSubject.GetRuleInfoAsync(CompositeRuleId); + var ruleInfo = await testSubject.GetRuleInfoAsync(compositeRuleId); ruleInfo.Should().BeNull(); logger.AssertNoOutputMessages(); @@ -65,10 +88,9 @@ public async Task GetRuleInfoAsync_NoActiveScope_ReturnsNull() [TestMethod] public async Task GetRuleInfoAsync_ServiceUnavailable_ReturnsNull() { - var testSubject = CreateTestSubject(out _, out var configScopeTrackerMock, out var logger); - SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope("id")); + SetUpRuleServiceProvider(false); - var ruleInfo = await testSubject.GetRuleInfoAsync(CompositeRuleId); + var ruleInfo = await testSubject.GetRuleInfoAsync(compositeRuleId); ruleInfo.Should().BeNull(); logger.AssertNoOutputMessages(); @@ -77,209 +99,136 @@ public async Task GetRuleInfoAsync_ServiceUnavailable_ReturnsNull() [TestMethod] public void GetRuleInfoAsync_ServiceThrows_ReturnsNullAndLogs() { - var testSubject = - CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out var logger); - SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope("id")); - SetUpServiceProvider(serviceProviderMock, out var rulesServiceMock); - rulesServiceMock - .Setup(x => x.GetEffectiveRuleDetailsAsync(It.IsAny())) - .ThrowsAsync(new Exception("my message")); + MockGetEffectiveRuleDetailsAsyncThrows(); - var act = () => testSubject.GetRuleInfoAsync(CompositeRuleId); + var act = () => testSubject.GetRuleInfoAsync(compositeRuleId); act.Should().NotThrow(); - logger.AssertPartialOutputStringExists("my message"); + logger.AssertPartialOutputStringExists(errorMessage); } [TestMethod] public async Task GetRuleInfoAsync_ForIssue_NoActiveScope_ReturnsNull() { - var testSubject = - CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out var logger); - SetUpIssueServiceProvider(serviceProviderMock, out _); - SetUpConfigScopeTracker(configScopeTrackerMock, null); - var issueId = Guid.NewGuid(); + SetUpConfigScopeTracker(null); - var ruleInfo = await testSubject.GetRuleInfoAsync(default,issueId); + var ruleInfo = await testSubject.GetRuleInfoAsync(compositeRuleId, issueId); ruleInfo.Should().BeNull(); logger.AssertNoOutputMessages(); } [TestMethod] - public async Task GetRuleInfoAsync_ForIssue_ServiceUnavailable_ReturnsNull() + public async Task GetRuleInfoAsync_ForIssue_IssueServiceUnavailable_ReturnsResultFromRulesService() { - var testSubject = CreateTestSubject(out _, out var configScopeTrackerMock, out var logger); - SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope("id")); + SetUpIssueServiceProvider(false); + MockGetEffectiveRuleDetailsAsync(compositeRuleId.ToString(), configurationScope.Id); - var ruleInfo = await testSubject.GetRuleInfoAsync(default,Guid.NewGuid()); + var ruleInfo = await testSubject.GetRuleInfoAsync(compositeRuleId, issueId); - ruleInfo.Should().BeNull(); + ruleInfo.Should().NotBeNull(); logger.AssertNoOutputMessages(); + VerifyGetRuleDetailsWasCalled(compositeRuleId.ToString()); } [TestMethod] - public void GetRuleInfoAsync_ForIssue_ServiceThrows_ReturnsNullAndLogs() + public void GetRuleInfoAsync_ForIssue_IssueServiceThrows_ReturnsNullAndLogs() { - var testSubject = - CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out var logger); - SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope("id")); - SetUpIssueServiceProvider(serviceProviderMock, out var issueServiceMock); - issueServiceMock - .Setup(x => x.GetEffectiveIssueDetailsAsync(It.IsAny())) - .ThrowsAsync(new Exception("my message")); + MockGetEffectiveIssueDetailsAsyncThrows(); - var act = () => testSubject.GetRuleInfoAsync(default,Guid.NewGuid()); + var act = () => testSubject.GetRuleInfoAsync(compositeRuleId, issueId); act.Should().NotThrow(); - logger.AssertPartialOutputStringExists("my message"); + logger.AssertPartialOutputStringExists(errorMessage); } [TestMethod] - public async Task GetRuleInfoAsync_FilterableIssueNull_CallsGetEffectiveRuleDetailsAsync() + public async Task GetRuleInfoAsync_IssueIdNull_CallsGetEffectiveRuleDetailsAsync() { - var testSubject = - CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out _); - SetUpIssueServiceProvider(serviceProviderMock, out var issueServiceMock); - SetUpServiceProvider(serviceProviderMock, out var rulesServiceMock); - SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope("configscope")); - - await testSubject.GetRuleInfoAsync(CompositeRuleId, null); + await testSubject.GetRuleInfoAsync(compositeRuleId, null); - rulesServiceMock.Verify(x => x.GetEffectiveRuleDetailsAsync(It.Is(p => p.ruleKey == CompositeRuleId.ToString())), Times.Once); - issueServiceMock.Verify(x => x.GetEffectiveIssueDetailsAsync(It.IsAny()), Times.Never); + VerifyGetRuleDetailsWasCalled(compositeRuleId.ToString()); + VerifyIssueDetailsWasNotCalled(); } [TestMethod] - public async Task GetRuleInfoAsync_FilterableIssueIdNull_CallsGetEffectiveRuleDetailsAsync() + public async Task GetRuleInfoAsync_IssueIdNotNull_CallsGetEffectiveIssueDetailsAsync() { - var testSubject = - CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out _); - SetUpIssueServiceProvider(serviceProviderMock, out var issueServiceMock); - SetUpServiceProvider(serviceProviderMock, out var rulesServiceMock); - SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope("configscope")); - Guid? issueId = null; + MockGetEffectiveIssueDetailsAsync(issueId, configurationScope.Id); - await testSubject.GetRuleInfoAsync(CompositeRuleId, issueId); - - rulesServiceMock.Verify(x => x.GetEffectiveRuleDetailsAsync(It.Is(p => p.ruleKey == CompositeRuleId.ToString())), Times.Once); - issueServiceMock.Verify(x => x.GetEffectiveIssueDetailsAsync(It.IsAny()), Times.Never); - } + await testSubject.GetRuleInfoAsync(compositeRuleId, issueId); - [TestMethod] - public async Task GetRuleInfoAsync_FilterableIssueIdNotNull_CallsGetEffectiveIssueDetailsAsync() - { - var configScopeId = "configscope"; - var issueId = Guid.NewGuid(); - var testSubject = CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out _); - SetUpIssueServiceProvider(serviceProviderMock, out var issueServiceMock); - SetUpServiceProvider(serviceProviderMock, out var rulesServiceMock); - SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope(configScopeId)); - SetupIssuesService(issueServiceMock, issueId, configScopeId, CreateEffectiveIssueDetailsDto(new MQRModeDetails(default, default))); - - await testSubject.GetRuleInfoAsync(CompositeRuleId, issueId); - - rulesServiceMock.Verify(x => x.GetEffectiveRuleDetailsAsync(It.IsAny()), Times.Never); - issueServiceMock.Verify(x => x.GetEffectiveIssueDetailsAsync(It.Is(p => p.issueId == issueId)), Times.Once); + VerifyRuleDetailsWasNotCalled(); + VerifyGetIssueDetailsWasCalled(issueId); } [TestMethod] public async Task GetRuleInfoAsync_GetEffectiveIssueDetailsAsyncThrows_CallsGetEffectiveRuleDetailsAsync() { - var testSubject = - CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out _); - SetUpIssueServiceProvider(serviceProviderMock, out var issueServiceMock); - SetUpServiceProvider(serviceProviderMock, out var rulesServiceMock); - SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope("configscope")); - var issueId = Guid.NewGuid(); - issueServiceMock - .Setup(x => x.GetEffectiveIssueDetailsAsync(It.IsAny())) - .ThrowsAsync(new Exception("my message")); - - await testSubject.GetRuleInfoAsync(CompositeRuleId, issueId); - - rulesServiceMock.Verify(x => x.GetEffectiveRuleDetailsAsync(It.Is(p => p.ruleKey == CompositeRuleId.ToString())), Times.Once); - issueServiceMock.Verify(x => x.GetEffectiveIssueDetailsAsync(It.Is(p => p.issueId == issueId)), Times.Once); + MockGetEffectiveIssueDetailsAsyncThrows(); + + await testSubject.GetRuleInfoAsync(compositeRuleId, issueId); + + VerifyGetRuleDetailsWasCalled(compositeRuleId.ToString()); + VerifyGetIssueDetailsWasCalled(issueId); } [TestMethod] public async Task GetRuleInfoAsync_BothServicesThrow_ReturnsNull() { - var testSubject = - CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out _); - SetUpIssueServiceProvider(serviceProviderMock, out var issueServiceMock); - SetUpServiceProvider(serviceProviderMock, out var rulesServiceMock); - SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope("configscope")); - var issueId = Guid.NewGuid(); - issueServiceMock - .Setup(x => x.GetEffectiveIssueDetailsAsync(It.IsAny())) - .ThrowsAsync(new Exception("my message")); - rulesServiceMock - .Setup(x => x.GetEffectiveRuleDetailsAsync(It.IsAny())) - .ThrowsAsync(new Exception("my message")); - - var result = await testSubject.GetRuleInfoAsync(CompositeRuleId, issueId); + MockGetEffectiveIssueDetailsAsyncThrows(); + MockGetEffectiveRuleDetailsAsyncThrows(); + + var result = await testSubject.GetRuleInfoAsync(compositeRuleId, issueId); result.Should().BeNull(); - rulesServiceMock.Verify(x => x.GetEffectiveRuleDetailsAsync(It.Is(p => p.ruleKey == CompositeRuleId.ToString())), Times.Once); - issueServiceMock.Verify(x => x.GetEffectiveIssueDetailsAsync(It.Is(p => p.issueId == issueId)), Times.Once); + VerifyGetRuleDetailsWasCalled(compositeRuleId.ToString()); + VerifyGetIssueDetailsWasCalled(issueId); } - private static void SetUpConfigScopeTracker( - Mock configScopeTrackerMock, - ConfigurationScope scope) => - configScopeTrackerMock.SetupGet(x => x.Current).Returns(scope); - - private static void SetupIssuesService( - Mock issuesServiceMock, - Guid id, - string configScopeId, - EffectiveIssueDetailsDto response) => - issuesServiceMock - .Setup(r => r.GetEffectiveIssueDetailsAsync(It.Is(p => p.configurationScopeId == configScopeId && p.issueId == id))) - .ReturnsAsync(new GetEffectiveIssueDetailsResponse(response)); - - private static void SetUpServiceProvider( - Mock serviceProviderMock, - out Mock rulesServiceMock) - { - rulesServiceMock = new Mock(); - var rulesService = rulesServiceMock.Object; - serviceProviderMock.Setup(x => x.TryGetTransientService(out rulesService)).Returns(true); - } + private void SetUpConfigScopeTracker(ConfigurationScope scope) => configScopeTrackerMock.Current.Returns(scope); - private static void SetUpIssueServiceProvider( - Mock serviceProviderMock, - out Mock rulesServiceMock) - { - rulesServiceMock = new Mock(); - var rulesService = rulesServiceMock.Object; - serviceProviderMock.Setup(x => x.TryGetTransientService(out rulesService)).Returns(true); - } + private void MockGetEffectiveIssueDetailsAsyncThrows() => issueServiceMock.GetEffectiveIssueDetailsAsync(Arg.Any()).ThrowsAsync(new Exception(errorMessage)); + + private void MockGetEffectiveRuleDetailsAsyncThrows() => rulesServiceMock.GetEffectiveRuleDetailsAsync(Arg.Any()).ThrowsAsync(new Exception(errorMessage)); + + private void MockGetEffectiveIssueDetailsAsync(Guid id, string configScopeId) => + issueServiceMock.GetEffectiveIssueDetailsAsync(Arg.Is(x => x.configurationScopeId == configScopeId && x.issueId == id)) + .Returns(new GetEffectiveIssueDetailsResponse(effectiveIssueDetailsDto)); + + private void MockGetEffectiveRuleDetailsAsync(string ruleKey, string configScopeId) => + rulesServiceMock.GetEffectiveRuleDetailsAsync(Arg.Is(x => x.configurationScopeId == configScopeId && x.ruleKey == ruleKey)) + .Returns(new GetEffectiveRuleDetailsResponse(default)); - private static SLCoreRuleMetaDataProvider CreateTestSubject( - out Mock serviceProviderMock, - out Mock configScopeTrackerMock, - out TestLogger logger) + private void SetUpRuleServiceProvider(bool result) => + serviceProviderMock.TryGetTransientService(out Arg.Any()).Returns(callInfo => + { + callInfo[0] = rulesServiceMock; + return result; + }); + + private void SetUpIssueServiceProvider(bool result) => + serviceProviderMock.TryGetTransientService(out Arg.Any()).Returns(callInfo => + { + callInfo[0] = issueServiceMock; + return result; + }); + + private void MockupServices() { - serviceProviderMock = new Mock(); - configScopeTrackerMock = new Mock(); - configScopeTrackerMock = new Mock(); - var ruleInfoConverter = new Mock(); - ruleInfoConverter.Setup(x => x.Convert(It.IsAny())).Returns(new RuleInfo(default, default, default, default, default, default, default, default)); - logger = new TestLogger(); - return new SLCoreRuleMetaDataProvider(serviceProviderMock.Object, configScopeTrackerMock.Object, ruleInfoConverter.Object, logger); + MockRuleInfoConverter(); + SetUpIssueServiceProvider(true); + SetUpRuleServiceProvider(true); + SetUpConfigScopeTracker(configurationScope); } - private static EffectiveIssueDetailsDto CreateEffectiveIssueDetailsDto(Either severityDetails, - Either description = default) => - new( - default, - default, - default, - default, - description, - default, - severityDetails, - default); + private void MockRuleInfoConverter() => ruleInfoConverter.Convert(Arg.Any()).Returns(defaultRuleInfo); + + private void VerifyGetIssueDetailsWasCalled(Guid id) => issueServiceMock.Received(1).GetEffectiveIssueDetailsAsync(Arg.Is(x => x.issueId == id)); + + private void VerifyGetRuleDetailsWasCalled(string ruleKey) => rulesServiceMock.Received(1).GetEffectiveRuleDetailsAsync(Arg.Is(x => x.ruleKey == ruleKey)); + + private void VerifyIssueDetailsWasNotCalled() => issueServiceMock.DidNotReceive().GetEffectiveIssueDetailsAsync(Arg.Any()); + + private void VerifyRuleDetailsWasNotCalled() => rulesServiceMock.DidNotReceive().GetEffectiveRuleDetailsAsync(Arg.Any()); } diff --git a/src/Education/Education.cs b/src/Education/Education.cs index c2bb19d406..8e2737615d 100644 --- a/src/Education/Education.cs +++ b/src/Education/Education.cs @@ -21,97 +21,96 @@ using System.ComponentModel.Composition; using Microsoft.VisualStudio.Threading; using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Suppressions; using SonarLint.VisualStudio.Education.Rule; using SonarLint.VisualStudio.Education.XamlGenerator; using SonarLint.VisualStudio.Infrastructure.VS; -namespace SonarLint.VisualStudio.Education -{ - [Export(typeof(IEducation))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal class Education : IEducation - { - private readonly IToolWindowService toolWindowService; - private readonly IRuleMetaDataProvider ruleMetadataProvider; - private readonly IRuleHelpXamlBuilder ruleHelpXamlBuilder; - private readonly IShowRuleInBrowser showRuleInBrowser; - private readonly ILogger logger; - private readonly IThreadHandling threadHandling; +namespace SonarLint.VisualStudio.Education; - private IRuleHelpToolWindow ruleHelpToolWindow; +[Export(typeof(IEducation))] +[PartCreationPolicy(CreationPolicy.Shared)] +internal class Education : IEducation +{ + private readonly ILogger logger; + private readonly IRuleHelpXamlBuilder ruleHelpXamlBuilder; + private readonly IRuleMetaDataProvider ruleMetadataProvider; + private readonly IShowRuleInBrowser showRuleInBrowser; + private readonly IThreadHandling threadHandling; + private readonly IToolWindowService toolWindowService; - [ImportingConstructor] - public Education(IToolWindowService toolWindowService, IRuleMetaDataProvider ruleMetadataProvider, IShowRuleInBrowser showRuleInBrowser, ILogger logger, IRuleHelpXamlBuilder ruleHelpXamlBuilder) - : this(toolWindowService, - ruleMetadataProvider, - showRuleInBrowser, - logger, - ruleHelpXamlBuilder, - ThreadHandling.Instance) { } + private IRuleHelpToolWindow ruleHelpToolWindow; - internal /* for testing */ Education(IToolWindowService toolWindowService, - IRuleMetaDataProvider ruleMetadataProvider, - IShowRuleInBrowser showRuleInBrowser, - ILogger logger, - IRuleHelpXamlBuilder ruleHelpXamlBuilder, - IThreadHandling threadHandling) - { - this.toolWindowService = toolWindowService; - this.ruleHelpXamlBuilder = ruleHelpXamlBuilder; - this.ruleMetadataProvider = ruleMetadataProvider; - this.showRuleInBrowser = showRuleInBrowser; - this.logger = logger; - this.threadHandling = threadHandling; - } + [ImportingConstructor] + public Education( + IToolWindowService toolWindowService, + IRuleMetaDataProvider ruleMetadataProvider, + IShowRuleInBrowser showRuleInBrowser, + ILogger logger, + IRuleHelpXamlBuilder ruleHelpXamlBuilder) + : this(toolWindowService, + ruleMetadataProvider, + showRuleInBrowser, + logger, + ruleHelpXamlBuilder, + ThreadHandling.Instance) + { + } - public void ShowRuleHelp(SonarCompositeRuleId ruleId, Guid? issueId, string issueContext) - { - ShowRuleHelpAsync(ruleId, issueId, issueContext).Forget(); - } + internal /* for testing */ Education( + IToolWindowService toolWindowService, + IRuleMetaDataProvider ruleMetadataProvider, + IShowRuleInBrowser showRuleInBrowser, + ILogger logger, + IRuleHelpXamlBuilder ruleHelpXamlBuilder, + IThreadHandling threadHandling) + { + this.toolWindowService = toolWindowService; + this.ruleHelpXamlBuilder = ruleHelpXamlBuilder; + this.ruleMetadataProvider = ruleMetadataProvider; + this.showRuleInBrowser = showRuleInBrowser; + this.logger = logger; + this.threadHandling = threadHandling; + } - private async Task ShowRuleHelpAsync(SonarCompositeRuleId ruleId, Guid? issueId, string issueContext) - { - await threadHandling.SwitchToBackgroundThread(); + public void ShowRuleHelp(SonarCompositeRuleId ruleId, Guid? issueId, string issueContext) => ShowRuleHelpAsync(ruleId, issueId, issueContext).Forget(); - var ruleInfo = await ruleMetadataProvider.GetRuleInfoAsync(ruleId, issueId); + private async Task ShowRuleHelpAsync(SonarCompositeRuleId ruleId, Guid? issueId, string issueContext) + { + await threadHandling.SwitchToBackgroundThread(); - await threadHandling.RunOnUIThreadAsync(() => - { - if (ruleInfo == null) - { - showRuleInBrowser.ShowRuleDescription(ruleId); - } - else - { - ShowRuleInIde(ruleInfo, ruleId, issueContext); - } - }); - } + var ruleInfo = await ruleMetadataProvider.GetRuleInfoAsync(ruleId, issueId); - private void ShowRuleInIde(IRuleInfo ruleInfo, SonarCompositeRuleId ruleId, string issueContext) + await threadHandling.RunOnUIThreadAsync(() => { - threadHandling.ThrowIfNotOnUIThread(); - - // Lazily fetch the tool window from a UI thread - if (ruleHelpToolWindow == null) + if (ruleInfo == null) { - ruleHelpToolWindow = toolWindowService.GetToolWindow(); + showRuleInBrowser.ShowRuleDescription(ruleId); } - - try + else { - var flowDocument = ruleHelpXamlBuilder.Create(ruleInfo, issueContext); + ShowRuleInIde(ruleInfo, ruleId, issueContext); + } + }); + } - ruleHelpToolWindow.UpdateContent(flowDocument); + private void ShowRuleInIde(IRuleInfo ruleInfo, SonarCompositeRuleId ruleId, string issueContext) + { + threadHandling.ThrowIfNotOnUIThread(); + // Lazily fetch the tool window from a UI thread + ruleHelpToolWindow ??= toolWindowService.GetToolWindow(); - toolWindowService.Show(RuleHelpToolWindow.ToolWindowId); - } - catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) - { - logger.WriteLine(string.Format(Resources.ERR_RuleHelpToolWindow_Exception, ex)); - showRuleInBrowser.ShowRuleDescription(ruleId); - } + try + { + var flowDocument = ruleHelpXamlBuilder.Create(ruleInfo, issueContext); + + ruleHelpToolWindow.UpdateContent(flowDocument); + + toolWindowService.Show(RuleHelpToolWindow.ToolWindowId); + } + catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) + { + logger.WriteLine(string.Format(Resources.ERR_RuleHelpToolWindow_Exception, ex)); + showRuleInBrowser.ShowRuleDescription(ruleId); } } } diff --git a/src/Education/ErrorList/SonarErrorListEventProcessor.cs b/src/Education/ErrorList/SonarErrorListEventProcessor.cs index 5c3828a5a2..abfd9c6cb7 100644 --- a/src/Education/ErrorList/SonarErrorListEventProcessor.cs +++ b/src/Education/ErrorList/SonarErrorListEventProcessor.cs @@ -23,50 +23,35 @@ using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Infrastructure.VS; -namespace SonarLint.VisualStudio.Education +namespace SonarLint.VisualStudio.Education.ErrorList; + +/// +/// Processor class that lets us handle WPF events from the Error List +/// +internal class SonarErrorListEventProcessor(IEducation educationService, IErrorListHelper errorListHelper, ILogger logger) : TableControlEventProcessorBase { - namespace SonarLint.VisualStudio.Education.ErrorList + public override void PreprocessNavigateToHelp( + ITableEntryHandle entry, + TableEntryEventArgs e) { - /// - /// Proccessor class that lets us handle WPF events from the Error List - /// - internal class SonarErrorListEventProcessor : TableControlEventProcessorBase - { - private readonly IEducation educationService; - private readonly IErrorListHelper errorListHelper; - private readonly ILogger logger; - - public SonarErrorListEventProcessor(IEducation educationService, IErrorListHelper errorListHelper, ILogger logger) - { - this.educationService = educationService; - this.errorListHelper = errorListHelper; - this.logger = logger; - } - - public override void PreprocessNavigateToHelp( - ITableEntryHandle entry, - TableEntryEventArgs e) - { - // If the user is navigating to help for one of the Sonar rules, - // show our rule description tool window + // If the user is navigating to help for one of the Sonar rules, + // show our rule description tool window - Requires.NotNull(entry, nameof(entry)); + Requires.NotNull(entry, nameof(entry)); - bool handled = false; + var handled = false; - if (errorListHelper.TryGetRuleId(entry, out var ruleId)) - { - errorListHelper.TryGetFilterableIssue(entry, out var filterableIssue); - logger.LogVerbose(Resources.ErrorList_Processor_SonarRuleDetected, ruleId); - - educationService.ShowRuleHelp(ruleId, filterableIssue?.IssueId, /* todo by SLVS-1630 */null); + if (errorListHelper.TryGetRuleId(entry, out var ruleId)) + { + errorListHelper.TryGetFilterableIssue(entry, out var filterableIssue); + logger.LogVerbose(Resources.ErrorList_Processor_SonarRuleDetected, ruleId); - // Mark the event as handled to stop the normal VS "show help in browser" behaviour - handled = true; - } + educationService.ShowRuleHelp(ruleId, filterableIssue?.IssueId, /* todo by SLVS-1630 */null); - e.Handled = handled; - } + // Mark the event as handled to stop the normal VS "show help in browser" behaviour + handled = true; } + + e.Handled = handled; } } diff --git a/src/Education/ErrorList/SonarErrorListEventProcessorProvider.cs b/src/Education/ErrorList/SonarErrorListEventProcessorProvider.cs index c385a8fa24..f4deed3a94 100644 --- a/src/Education/ErrorList/SonarErrorListEventProcessorProvider.cs +++ b/src/Education/ErrorList/SonarErrorListEventProcessorProvider.cs @@ -24,43 +24,38 @@ using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Infrastructure.VS; -namespace SonarLint.VisualStudio.Education -{ - namespace SonarLint.VisualStudio.Education.ErrorList - { - // Notifies VS that we want to handle events from the Error List +namespace SonarLint.VisualStudio.Education.ErrorList; +// Notifies VS that we want to handle events from the Error List - [Export(typeof(ITableControlEventProcessorProvider))] - [Name("SonarLint ErrorList Event Processor")] +[Export(typeof(ITableControlEventProcessorProvider))] +[Name("SonarLint ErrorList Event Processor")] - // Need to hook into the list of processors before the standard VS handler so we can - // change the behaviour of the "navigate to help" action - [Order(After = "Default Priority", Before = "ErrorListPackage Table Control Event Processor")] - [ManagerType("ErrorsTable")] +// Need to hook into the list of processors before the standard VS handler so we can +// change the behaviour of the "navigate to help" action +[Order(After = "Default Priority", Before = "ErrorListPackage Table Control Event Processor")] +[ManagerType("ErrorsTable")] - // TODO - DataSourceType/DataSource can both be used multiple times. Can we just register for our source and Roslyn? - // Ideally, we'd only handle our own data source types. However, we also need to handle the Roslyn data source - [DataSourceType("*")] - [DataSource("*")] - internal class SonarErrorListEventProcessorProvider : ITableControlEventProcessorProvider - { - private readonly IEducation educationService; - private readonly IErrorListHelper errorListHelper; - private readonly ILogger logger; +// TODO - DataSourceType/DataSource can both be used multiple times. Can we just register for our source and Roslyn? +// Ideally, we'd only handle our own data source types. However, we also need to handle the Roslyn data source +[DataSourceType("*")] +[DataSource("*")] +internal class SonarErrorListEventProcessorProvider : ITableControlEventProcessorProvider +{ + private readonly IEducation educationService; + private readonly IErrorListHelper errorListHelper; + private readonly ILogger logger; - [ImportingConstructor] - public SonarErrorListEventProcessorProvider(IEducation educationService, IErrorListHelper errorListHelper, ILogger logger) - { - this.educationService = educationService; - this.errorListHelper = errorListHelper; - this.logger = logger; - } + [ImportingConstructor] + public SonarErrorListEventProcessorProvider(IEducation educationService, IErrorListHelper errorListHelper, ILogger logger) + { + this.educationService = educationService; + this.errorListHelper = errorListHelper; + this.logger = logger; + } - public ITableControlEventProcessor GetAssociatedEventProcessor(IWpfTableControl tableControl) - { - logger.LogVerbose(Resources.ErrorList_ProcessorCreated); - return new SonarErrorListEventProcessor(educationService, errorListHelper, logger); - } - } + public ITableControlEventProcessor GetAssociatedEventProcessor(IWpfTableControl tableControl) + { + logger.LogVerbose(Resources.ErrorList_ProcessorCreated); + return new SonarErrorListEventProcessor(educationService, errorListHelper, logger); } } diff --git a/src/Education/Rule/SLCoreRuleMetaDataProvider.cs b/src/Education/Rule/SLCoreRuleMetaDataProvider.cs index 8986f656e9..22f2231e6c 100644 --- a/src/Education/Rule/SLCoreRuleMetaDataProvider.cs +++ b/src/Education/Rule/SLCoreRuleMetaDataProvider.cs @@ -29,25 +29,14 @@ namespace SonarLint.VisualStudio.Education.Rule; [Export(typeof(IRuleMetaDataProvider))] [PartCreationPolicy(CreationPolicy.Shared)] -internal class SLCoreRuleMetaDataProvider : IRuleMetaDataProvider +[method: ImportingConstructor] +internal class SLCoreRuleMetaDataProvider( + ISLCoreServiceProvider slCoreServiceProvider, + IActiveConfigScopeTracker activeConfigScopeTracker, + IRuleInfoConverter ruleInfoConverter, + ILogger logger) + : IRuleMetaDataProvider { - private readonly IActiveConfigScopeTracker activeConfigScopeTracker; - private readonly IRuleInfoConverter ruleInfoConverter; - private readonly ILogger logger; - private readonly ISLCoreServiceProvider slCoreServiceProvider; - - [ImportingConstructor] - public SLCoreRuleMetaDataProvider(ISLCoreServiceProvider slCoreServiceProvider, - IActiveConfigScopeTracker activeConfigScopeTracker, - IRuleInfoConverter ruleInfoConverter, - ILogger logger) - { - this.slCoreServiceProvider = slCoreServiceProvider; - this.activeConfigScopeTracker = activeConfigScopeTracker; - this.ruleInfoConverter = ruleInfoConverter; - this.logger = logger; - } - /// public async Task GetRuleInfoAsync(SonarCompositeRuleId ruleId, Guid? issueId = null) { diff --git a/src/Infrastructure.VS.UnitTests/ErrorListHelperTests.cs b/src/Infrastructure.VS.UnitTests/ErrorListHelperTests.cs index ec70d46d49..4a9734cce4 100644 --- a/src/Infrastructure.VS.UnitTests/ErrorListHelperTests.cs +++ b/src/Infrastructure.VS.UnitTests/ErrorListHelperTests.cs @@ -22,538 +22,480 @@ using Microsoft.VisualStudio.Shell.Interop; using Microsoft.VisualStudio.Shell.TableControl; using Microsoft.VisualStudio.Shell.TableManager; -using Moq; using SonarLint.VisualStudio.IssueVisualization.Models; -namespace SonarLint.VisualStudio.Infrastructure.VS.UnitTests +namespace SonarLint.VisualStudio.Infrastructure.VS.UnitTests; + +[TestClass] +public class ErrorListHelperTests { - [TestClass] - public class ErrorListHelperTests + private IAnalysisIssueVisualization issueMock; + private ErrorListHelper testSubject; + private IVsUIServiceOperation vsUiServiceOperation; + + [TestInitialize] + public void TestInitialize() { - internal enum TestVsSuppressionState - { - Active, - Suppressed, - NotApplicable, - } + vsUiServiceOperation = Substitute.For(); + issueMock = Substitute.For(); - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport()); - } + testSubject = new ErrorListHelper(vsUiServiceOperation); + } - [TestMethod] - public void MefCtor_CheckIsSingleton() - => MefTestHelpers.CheckIsSingletonMefComponent(); + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport()); - [TestMethod] - public void MefCtor_DoesNotCallAnyServices() - { - var serviceOp = new Mock(); + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); - _ = new ErrorListHelper(serviceOp.Object); + [TestMethod] + public void MefCtor_DoesNotCallAnyServices() => + // The MEF constructor should be free-threaded, which it will be if + // it doesn't make any external calls. + vsUiServiceOperation.ReceivedCalls().Should().BeEmpty(); - // The MEF constructor should be free-threaded, which it will be if - // it doesn't make any external calls. - serviceOp.Invocations.Should().BeEmpty(); - } - - [TestMethod] - public void TryGetIssueFromSelectedRow_SingleSonarIssue_IssueReturned() - { - var issueMock = Mock.Of(); - var issueHandle = CreateIssueHandle(111, new Dictionary + [TestMethod] + public void TryGetIssueFromSelectedRow_SingleSonarIssue_IssueReturned() + { + var issueHandle = CreateIssueHandle(111, + new Dictionary { { StandardTableKeyNames.BuildTool, "SonarLint" }, - { StandardTableKeyNames.ErrorCode, "javascript:S333"}, + { StandardTableKeyNames.ErrorCode, "javascript:S333" }, { SonarLintTableControlConstants.IssueVizColumnName, issueMock } }); - var errorList = CreateErrorList(issueHandle); - var serviceProvider = CreateServiceOperation(errorList); + MockErrorList(issueHandle); - var testSubject = new ErrorListHelper(serviceProvider); - bool result = testSubject.TryGetIssueFromSelectedRow(out var issue); + var result = testSubject.TryGetIssueFromSelectedRow(out var issue); - result.Should().BeTrue(); - issue.Should().BeSameAs(issueMock); - } + result.Should().BeTrue(); + issue.Should().BeSameAs(issueMock); + } - [TestMethod] - public void TryGetIssueFromSelectedRow_SingleItemButNoAnalysisIssue_IssueNotReturned() - { - var issueHandle = CreateIssueHandle(111, new Dictionary + [TestMethod] + public void TryGetIssueFromSelectedRow_SingleItemButNoAnalysisIssue_IssueNotReturned() + { + var issueHandle = CreateIssueHandle(111, + new Dictionary { { StandardTableKeyNames.BuildTool, "SonarLint" }, - { StandardTableKeyNames.ErrorCode, "javascript:S333"}, + { StandardTableKeyNames.ErrorCode, "javascript:S333" }, { SonarLintTableControlConstants.IssueVizColumnName, null } }); - var errorList = CreateErrorList(issueHandle); - var serviceProvider = CreateServiceOperation(errorList); + MockErrorList(issueHandle); - var testSubject = new ErrorListHelper(serviceProvider); - var result = testSubject.TryGetIssueFromSelectedRow(out _); + var result = testSubject.TryGetIssueFromSelectedRow(out _); - result.Should().BeFalse(); - } + result.Should().BeFalse(); + } - [TestMethod] - public void TryGetIssueFromSelectedRow_MultipleItemsSelected_IssueNotReturned() - { - var cppIssueHandle = CreateIssueHandle(111, new Dictionary + [TestMethod] + public void TryGetIssueFromSelectedRow_MultipleItemsSelected_IssueNotReturned() + { + var cppIssueHandle = CreateIssueHandle(111, + new Dictionary { { StandardTableKeyNames.BuildTool, "SonarLint" }, { StandardTableKeyNames.ErrorCode, "cpp:S222" }, - { SonarLintTableControlConstants.IssueVizColumnName, Mock.Of() } + { SonarLintTableControlConstants.IssueVizColumnName, Substitute.For() } }); - var jsIssueHandle = CreateIssueHandle(222, new Dictionary + var jsIssueHandle = CreateIssueHandle(222, + new Dictionary { { StandardTableKeyNames.BuildTool, "SonarLint.CSharp" }, { StandardTableKeyNames.ErrorCode, "csharpsquid:S222" }, - { SonarLintTableControlConstants.IssueVizColumnName, Mock.Of() } + { SonarLintTableControlConstants.IssueVizColumnName, Substitute.For() } }); + MockErrorList(cppIssueHandle, jsIssueHandle); - var errorList = CreateErrorList(cppIssueHandle, jsIssueHandle); - var serviceProvider = CreateServiceOperation(errorList); + var result = testSubject.TryGetIssueFromSelectedRow(out _); - var testSubject = new ErrorListHelper(serviceProvider); - var result = testSubject.TryGetIssueFromSelectedRow(out _); - - result.Should().BeFalse(); - } + result.Should().BeFalse(); + } - [TestMethod] - public void TryGetRoslynIssueFromSelectedRow_SingleRoslynIssue_IssueReturned() - { - var path = "filepath"; - var line = 12; - var column = 101; - var errorCode = "javascript:S333"; - var issueHandle = CreateIssueHandle(111, new Dictionary + [TestMethod] + public void TryGetRoslynIssueFromSelectedRow_SingleRoslynIssue_IssueReturned() + { + var path = "filepath"; + var line = 12; + var column = 101; + var errorCode = "javascript:S333"; + var issueHandle = CreateIssueHandle(111, + new Dictionary { { StandardTableKeyNames.BuildTool, "SonarLint" }, - { StandardTableKeyNames.ErrorCode, errorCode}, + { StandardTableKeyNames.ErrorCode, errorCode }, { StandardTableKeyNames.DocumentName, path }, { StandardTableKeyNames.Line, line }, { StandardTableKeyNames.Column, column } }); - var errorList = CreateErrorList(issueHandle); - var serviceProvider = CreateServiceOperation(errorList); - - var testSubject = new ErrorListHelper(serviceProvider); - bool result = testSubject.TryGetRoslynIssueFromSelectedRow(out var issue); - - result.Should().BeTrue(); - issue.RuleId.Should().BeSameAs(errorCode); - issue.FilePath.Should().BeSameAs(path); - issue.StartLine.Should().Be(line + 1); - issue.RoslynStartLine.Should().Be(line + 1); - issue.RoslynStartColumn.Should().Be(column + 1); - issue.LineHash.Should().BeNull(); - } + MockErrorList(issueHandle); - [TestMethod] - public void TryGetRoslynIssueFromSelectedRow_NonSonarIssue_NothingReturned() - { - var issueHandle = CreateIssueHandle(111, new Dictionary + var result = testSubject.TryGetRoslynIssueFromSelectedRow(out var issue); + + result.Should().BeTrue(); + issue.RuleId.Should().BeSameAs(errorCode); + issue.FilePath.Should().BeSameAs(path); + issue.StartLine.Should().Be(line + 1); + issue.RoslynStartLine.Should().Be(line + 1); + issue.RoslynStartColumn.Should().Be(column + 1); + issue.LineHash.Should().BeNull(); + } + + [TestMethod] + public void TryGetRoslynIssueFromSelectedRow_NonSonarIssue_NothingReturned() + { + var issueHandle = CreateIssueHandle(111, + new Dictionary { { StandardTableKeyNames.BuildTool, "Not SonarLint" }, - { StandardTableKeyNames.ErrorCode, "javascript:S333"}, + { StandardTableKeyNames.ErrorCode, "javascript:S333" }, { StandardTableKeyNames.DocumentName, "filepath" }, { StandardTableKeyNames.Line, 1 }, { StandardTableKeyNames.Column, 2 } }); - var errorList = CreateErrorList(issueHandle); - var serviceProvider = CreateServiceOperation(errorList); + MockErrorList(issueHandle); - var testSubject = new ErrorListHelper(serviceProvider); - bool result = testSubject.TryGetRoslynIssueFromSelectedRow(out _); + var result = testSubject.TryGetRoslynIssueFromSelectedRow(out _); - result.Should().BeFalse(); - } + result.Should().BeFalse(); + } - [TestMethod] - public void TryGetRoslynIssueFromSelectedRow_NoFilePath_NothingReturned() - { - var issueHandle = CreateIssueHandle(111, new Dictionary + [TestMethod] + public void TryGetRoslynIssueFromSelectedRow_NoFilePath_NothingReturned() + { + var issueHandle = CreateIssueHandle(111, + new Dictionary { { StandardTableKeyNames.BuildTool, "SonarLint" }, - { StandardTableKeyNames.ErrorCode, "javascript:S333"}, - { StandardTableKeyNames.Line, 1 }, - { StandardTableKeyNames.Column, 2 } + { StandardTableKeyNames.ErrorCode, "javascript:S333" }, + { StandardTableKeyNames.Line, 1 }, { StandardTableKeyNames.Column, 2 } }); - var errorList = CreateErrorList(issueHandle); - var serviceProvider = CreateServiceOperation(errorList); + MockErrorList(issueHandle); - var testSubject = new ErrorListHelper(serviceProvider); - bool result = testSubject.TryGetRoslynIssueFromSelectedRow(out _); + var result = testSubject.TryGetRoslynIssueFromSelectedRow(out _); - result.Should().BeFalse(); - } + result.Should().BeFalse(); + } - [TestMethod] - public void TryGetRoslynIssueFromSelectedRow_NoStartLine_NothingReturned() - { - var issueHandle = CreateIssueHandle(111, new Dictionary + [TestMethod] + public void TryGetRoslynIssueFromSelectedRow_NoStartLine_NothingReturned() + { + var issueHandle = CreateIssueHandle(111, + new Dictionary { { StandardTableKeyNames.BuildTool, "SonarLint" }, - { StandardTableKeyNames.ErrorCode, "javascript:S333"}, + { StandardTableKeyNames.ErrorCode, "javascript:S333" }, { StandardTableKeyNames.DocumentName, "filepath" }, - { StandardTableKeyNames.Column, 2 }, + { StandardTableKeyNames.Column, 2 } }); - var errorList = CreateErrorList(issueHandle); - var serviceProvider = CreateServiceOperation(errorList); + MockErrorList(issueHandle); - var testSubject = new ErrorListHelper(serviceProvider); - bool result = testSubject.TryGetRoslynIssueFromSelectedRow(out _); + var result = testSubject.TryGetRoslynIssueFromSelectedRow(out _); - result.Should().BeFalse(); - } + result.Should().BeFalse(); + } - [TestMethod] - public void TryGetRoslynIssueFromSelectedRow_NoStartColumn_NothingReturned() - { - var issueHandle = CreateIssueHandle(111, new Dictionary + [TestMethod] + public void TryGetRoslynIssueFromSelectedRow_NoStartColumn_NothingReturned() + { + var issueHandle = CreateIssueHandle(111, + new Dictionary { { StandardTableKeyNames.BuildTool, "SonarLint" }, - { StandardTableKeyNames.ErrorCode, "javascript:S333"}, + { StandardTableKeyNames.ErrorCode, "javascript:S333" }, { StandardTableKeyNames.DocumentName, "filepath" }, - { StandardTableKeyNames.Line, 1 }, + { StandardTableKeyNames.Line, 1 } }); - var errorList = CreateErrorList(issueHandle); - var serviceProvider = CreateServiceOperation(errorList); + MockErrorList(issueHandle); - var testSubject = new ErrorListHelper(serviceProvider); - bool result = testSubject.TryGetRoslynIssueFromSelectedRow(out _); + var result = testSubject.TryGetRoslynIssueFromSelectedRow(out _); - result.Should().BeFalse(); - } - - [TestMethod] - [DataRow("S666", "csharpsquid", "S666", "SonarAnalyzer.CSharp", null)] - [DataRow("S666", "vbnet", "S666", "SonarAnalyzer.VisualBasic", null)] - [DataRow("S234", "vbnet", "S234", "SonarAnalyzer.VisualBasic", null)] - [DataRow("c:S111", "c", "S111", "SonarLint", null)] - [DataRow("cpp:S222", "cpp", "S222", "SonarLint", null)] - [DataRow("javascript:S333", "javascript", "S333", "SonarLint", null)] - [DataRow("typescript:S444", "typescript", "S444", "SonarLint", null)] - [DataRow("secrets:S555", "secrets", "S555", "SonarLint", null)] - [DataRow("foo:bar", "foo", "bar", "SonarLint", null)] - [DataRow("S666", "csharpsquid", "S666", null, "https://rules.sonarsource.com/csharp/RSPEC-666/")] - [DataRow("S666", "vbnet", "S666", null, "https://rules.sonarsource.com/vbnet/RSPEC-666/")] - [DataRow("S234", "vbnet", "S234", null, "https://rules.sonarsource.com/vbnet/RSPEC-234/")] - public void TryGetRuleIdFromSelectedRow_SingleSonarIssue_ErrorCodeReturned(string fullRuleKey, string expectedRepo, string expectedRule, string buildTool, string helpLink) - { - // Arrange - var issueHandle = CreateIssueHandle(111, new Dictionary - { - { StandardTableKeyNames.BuildTool, buildTool }, - { StandardTableKeyNames.HelpLink, helpLink }, - { StandardTableKeyNames.ErrorCode, fullRuleKey } - }); - - var errorList = CreateErrorList(issueHandle); - var serviceProvider = CreateServiceOperation(errorList); + result.Should().BeFalse(); + } - // Act - var testSubject = new ErrorListHelper(serviceProvider); - bool result = testSubject.TryGetRuleIdFromSelectedRow(out var ruleId); + [TestMethod] + [DataRow("S666", "csharpsquid", "S666", "SonarAnalyzer.CSharp", null)] + [DataRow("S666", "vbnet", "S666", "SonarAnalyzer.VisualBasic", null)] + [DataRow("S234", "vbnet", "S234", "SonarAnalyzer.VisualBasic", null)] + [DataRow("c:S111", "c", "S111", "SonarLint", null)] + [DataRow("cpp:S222", "cpp", "S222", "SonarLint", null)] + [DataRow("javascript:S333", "javascript", "S333", "SonarLint", null)] + [DataRow("typescript:S444", "typescript", "S444", "SonarLint", null)] + [DataRow("secrets:S555", "secrets", "S555", "SonarLint", null)] + [DataRow("foo:bar", "foo", "bar", "SonarLint", null)] + [DataRow("S666", "csharpsquid", "S666", null, "https://rules.sonarsource.com/csharp/RSPEC-666/")] + [DataRow("S666", "vbnet", "S666", null, "https://rules.sonarsource.com/vbnet/RSPEC-666/")] + [DataRow("S234", "vbnet", "S234", null, "https://rules.sonarsource.com/vbnet/RSPEC-234/")] + public void TryGetRuleIdFromSelectedRow_SingleSonarIssue_ErrorCodeReturned( + string fullRuleKey, + string expectedRepo, + string expectedRule, + string buildTool, + string helpLink) + { + var issueHandle = CreateIssueHandle(111, + new Dictionary { { StandardTableKeyNames.BuildTool, buildTool }, { StandardTableKeyNames.HelpLink, helpLink }, { StandardTableKeyNames.ErrorCode, fullRuleKey } }); + MockErrorList(issueHandle); - // Assert - result.Should().BeTrue(); - ruleId.RepoKey.Should().Be(expectedRepo); - ruleId.RuleKey.Should().Be(expectedRule); - } + var result = testSubject.TryGetRuleIdFromSelectedRow(out var ruleId); - [TestMethod] - [DataRow("S666", "csharpsquid", "S666", "SonarAnalyzer.CSharp", null)] - [DataRow("S666", "vbnet", "S666", "SonarAnalyzer.VisualBasic", null)] - [DataRow("S234", "vbnet", "S234", "SonarAnalyzer.VisualBasic", null)] - [DataRow("c:S111", "c", "S111", "SonarLint", null)] - [DataRow("cpp:S222", "cpp", "S222", "SonarLint", null)] - [DataRow("javascript:S333", "javascript", "S333", "SonarLint", null)] - [DataRow("typescript:S444", "typescript", "S444", "SonarLint", null)] - [DataRow("secrets:S555", "secrets", "S555", "SonarLint", null)] - [DataRow("foo:bar", "foo", "bar", "SonarLint", null)] - [DataRow("S666", "csharpsquid", "S666", null, "https://rules.sonarsource.com/csharp/RSPEC-666/")] - [DataRow("S666", "vbnet", "S666", null, "https://rules.sonarsource.com/vbnet/RSPEC-666/")] - [DataRow("S234", "vbnet", "S234", null, "https://rules.sonarsource.com/vbnet/RSPEC-234/")] - public void TryGetRuleId_FromHandle_ErrorCodeReturned(string fullRuleKey, string expectedRepo, string expectedRule, string buildTool, string helpLink) - { - // Note: this is a copy of TryGetRuleIdFromSelectedRow_SingleSonarIssue_ErrorCodeReturned, - // but without the serviceProvider and IErrorList setup + result.Should().BeTrue(); + ruleId.RepoKey.Should().Be(expectedRepo); + ruleId.RuleKey.Should().Be(expectedRule); + } - // Arrange - var issueHandle = CreateIssueHandle(111, new Dictionary - { - { StandardTableKeyNames.BuildTool, buildTool }, - { StandardTableKeyNames.HelpLink, helpLink }, - { StandardTableKeyNames.ErrorCode, fullRuleKey } - }); + [TestMethod] + [DataRow("S666", "csharpsquid", "S666", "SonarAnalyzer.CSharp", null)] + [DataRow("S666", "vbnet", "S666", "SonarAnalyzer.VisualBasic", null)] + [DataRow("S234", "vbnet", "S234", "SonarAnalyzer.VisualBasic", null)] + [DataRow("c:S111", "c", "S111", "SonarLint", null)] + [DataRow("cpp:S222", "cpp", "S222", "SonarLint", null)] + [DataRow("javascript:S333", "javascript", "S333", "SonarLint", null)] + [DataRow("typescript:S444", "typescript", "S444", "SonarLint", null)] + [DataRow("secrets:S555", "secrets", "S555", "SonarLint", null)] + [DataRow("foo:bar", "foo", "bar", "SonarLint", null)] + [DataRow("S666", "csharpsquid", "S666", null, "https://rules.sonarsource.com/csharp/RSPEC-666/")] + [DataRow("S666", "vbnet", "S666", null, "https://rules.sonarsource.com/vbnet/RSPEC-666/")] + [DataRow("S234", "vbnet", "S234", null, "https://rules.sonarsource.com/vbnet/RSPEC-234/")] + public void TryGetRuleId_FromHandle_ErrorCodeReturned( + string fullRuleKey, + string expectedRepo, + string expectedRule, + string buildTool, + string helpLink) + { + // Note: this is a copy of TryGetRuleIdFromSelectedRow_SingleSonarIssue_ErrorCodeReturned, + // but without the serviceProvider and IErrorList setup + var issueHandle = CreateIssueHandle(111, + new Dictionary { { StandardTableKeyNames.BuildTool, buildTool }, { StandardTableKeyNames.HelpLink, helpLink }, { StandardTableKeyNames.ErrorCode, fullRuleKey } }); - // Act - var testSubject = new ErrorListHelper(Mock.Of()); - bool result = testSubject.TryGetRuleId(issueHandle, out var ruleId); + var result = testSubject.TryGetRuleId(issueHandle, out var ruleId); - // Assert - result.Should().BeTrue(); - ruleId.RepoKey.Should().Be(expectedRepo); - ruleId.RuleKey.Should().Be(expectedRule); - } + result.Should().BeTrue(); + ruleId.RepoKey.Should().Be(expectedRepo); + ruleId.RuleKey.Should().Be(expectedRule); + } - [TestMethod] - public void TryGetRuleIdFromSelectedRow_NonStandardErrorCode_NoException_ErrorCodeNotReturned() + [TestMethod] + public void TryGetRuleIdFromSelectedRow_NonStandardErrorCode_NoException_ErrorCodeNotReturned() + { + var issueHandle = CreateIssueHandle(111, new Dictionary { - // Arrange - var issueHandle = CreateIssueHandle(111, new Dictionary - { - { StandardTableKeyNames.BuildTool, "SonarLint" }, - { StandardTableKeyNames.ErrorCode, ":" } // should not happen - }); + { StandardTableKeyNames.BuildTool, "SonarLint" }, + { StandardTableKeyNames.ErrorCode, ":" } // should not happen + }); + MockErrorList(issueHandle); - var errorList = CreateErrorList(issueHandle); - var serviceProvider = CreateServiceOperation(errorList); + var result = testSubject.TryGetRuleIdFromSelectedRow(out var errorCode); - // Act - var testSubject = new ErrorListHelper(serviceProvider); - bool result = testSubject.TryGetRuleIdFromSelectedRow(out var errorCode); - - // Assert - result.Should().BeFalse(); - errorCode.Should().BeNull(); - } + result.Should().BeFalse(); + errorCode.Should().BeNull(); + } - [TestMethod] - public void TryGetRuleIdFromSelectedRow_MultipleItemsSelected_ErrorCodeNotReturned() + [TestMethod] + public void TryGetRuleIdFromSelectedRow_MultipleItemsSelected_ErrorCodeNotReturned() + { + var cppIssueHandle = CreateIssueHandle(111, new Dictionary { - var cppIssueHandle = CreateIssueHandle(111, new Dictionary - { - { StandardTableKeyNames.BuildTool, "SonarLint" }, - { StandardTableKeyNames.ErrorCode, "cpp:S222" } - }); - var jsIssueHandle = CreateIssueHandle(222, new Dictionary - { - { StandardTableKeyNames.BuildTool, "SonarLint.CSharp" }, - { StandardTableKeyNames.ErrorCode, "csharpsquid:S222" } - }); - - var errorList = CreateErrorList(cppIssueHandle, jsIssueHandle); - var serviceProvider = CreateServiceOperation(errorList); + { StandardTableKeyNames.BuildTool, "SonarLint" }, + { StandardTableKeyNames.ErrorCode, "cpp:S222" } + }); + var jsIssueHandle = CreateIssueHandle(222, new Dictionary + { + { StandardTableKeyNames.BuildTool, "SonarLint.CSharp" }, + { StandardTableKeyNames.ErrorCode, "csharpsquid:S222" } + }); + MockErrorList(cppIssueHandle, jsIssueHandle); - // Act - var testSubject = new ErrorListHelper(serviceProvider); - bool result = testSubject.TryGetRuleIdFromSelectedRow(out var errorCode); + var result = testSubject.TryGetRuleIdFromSelectedRow(out var errorCode); - // Assert - result.Should().BeFalse(); - errorCode.Should().BeNull(); - } + result.Should().BeFalse(); + errorCode.Should().BeNull(); + } - [TestMethod] - public void TryGetRuleIdFromSelectedRow_NotSonarLintIssue() + [TestMethod] + public void TryGetRuleIdFromSelectedRow_NotSonarLintIssue() + { + var issueHandle = CreateIssueHandle(111, new Dictionary { - // Arrange - var issueHandle = CreateIssueHandle(111, new Dictionary - { - { StandardTableKeyNames.BuildTool, new object() }, - { StandardTableKeyNames.ErrorCode, "cpp:S333" } - }); + { StandardTableKeyNames.BuildTool, new object() }, + { StandardTableKeyNames.ErrorCode, "cpp:S333" } + }); + MockErrorList(issueHandle); - var errorList = CreateErrorList(issueHandle); - var serviceProvider = CreateServiceOperation(errorList); + var result = testSubject.TryGetRuleIdFromSelectedRow(out var errorCode); - // Act - var testSubject = new ErrorListHelper(serviceProvider); - bool result = testSubject.TryGetRuleIdFromSelectedRow(out var errorCode); - - // Assert - result.Should().BeFalse(); - errorCode.Should().BeNull(); - } + result.Should().BeFalse(); + errorCode.Should().BeNull(); + } - [TestMethod] - [DataRow("cpp:S333", "AnotherAnalyzer", null)] - [DataRow("S666", "AnotherAnalyzerWithSonarHelpLink", "https://rules.sonarsource.com/csharp/RSPEC-666/")] - [DataRow("S234", "SomeOtherAnalyzer", "https://rules.sonarsource.com/vbnet/RSPEC-234/")] - public void TryGetRuleId_FromHandle_NotSonarLintIssue(string fullRuleKey, object buildTool, string helpLink) + [TestMethod] + [DataRow("cpp:S333", "AnotherAnalyzer", null)] + [DataRow("S666", "AnotherAnalyzerWithSonarHelpLink", "https://rules.sonarsource.com/csharp/RSPEC-666/")] + [DataRow("S234", "SomeOtherAnalyzer", "https://rules.sonarsource.com/vbnet/RSPEC-234/")] + public void TryGetRuleId_FromHandle_NotSonarLintIssue(string fullRuleKey, object buildTool, string helpLink) + { + // Note: this is a copy of TryGetRuleIdFromSelectedRow_SingleSonarIssue_ErrorCodeReturned, + // but without the serviceProvider and IErrorList setup + var issueHandle = CreateIssueHandle(111, new Dictionary { - // Note: this is a copy of TryGetRuleIdFromSelectedRow_SingleSonarIssue_ErrorCodeReturned, - // but without the serviceProvider and IErrorList setup + { StandardTableKeyNames.BuildTool, buildTool }, + { StandardTableKeyNames.HelpLink, helpLink }, + { StandardTableKeyNames.ErrorCode, fullRuleKey } + }); - // Arrange - var issueHandle = CreateIssueHandle(111, new Dictionary - { - { StandardTableKeyNames.BuildTool, buildTool }, - { StandardTableKeyNames.HelpLink, helpLink }, - { StandardTableKeyNames.ErrorCode, fullRuleKey } - }); - - // Act - var testSubject = new ErrorListHelper(Mock.Of()); - bool result = testSubject.TryGetRuleId(issueHandle, out var errorCode); + var result = testSubject.TryGetRuleId(issueHandle, out var errorCode); - // Assert - result.Should().BeFalse(); - errorCode.Should().BeNull(); - } + result.Should().BeFalse(); + errorCode.Should().BeNull(); + } - [TestMethod] - public void TryGetRuleIdAndSuppressionStateFromSelectedRow_NoSuppressionState_ReturnsIsNotSuppressed() + [TestMethod] + public void TryGetRuleIdAndSuppressionStateFromSelectedRow_NoSuppressionState_ReturnsIsNotSuppressed() + { + var issueHandle = CreateIssueHandle(111, new Dictionary { - // Arrange - var issueHandle = CreateIssueHandle(111, new Dictionary - { - { StandardTableKeyNames.BuildTool, "SonarLint" }, - { StandardTableKeyNames.ErrorCode, "cpp:S222" } - }); + { StandardTableKeyNames.BuildTool, "SonarLint" }, + { StandardTableKeyNames.ErrorCode, "cpp:S222" } + }); + MockErrorList(issueHandle); - var errorList = CreateErrorList(issueHandle); - var serviceProvider = CreateServiceOperation(errorList); + var result = testSubject.TryGetRuleIdAndSuppressionStateFromSelectedRow(out _, out var isSuppressed); - // Act - var testSubject = new ErrorListHelper(serviceProvider); - bool result = testSubject.TryGetRuleIdAndSuppressionStateFromSelectedRow(out var ruleId, out var isSuppressed); - - // Assert - result.Should().BeTrue(); - isSuppressed.Should().BeFalse(); - } + result.Should().BeTrue(); + isSuppressed.Should().BeFalse(); + } - [DataTestMethod] - [DataRow(SuppressionState.Suppressed, true)] - [DataRow(SuppressionState.NotApplicable, false)] - [DataRow(SuppressionState.Active, false)] - public void TryGetRuleIdAndSuppressionStateFromSelectedRow_NoSuppressionState_ReturnsIsNotSuppressed(SuppressionState suppressionState, bool expectedSuppression) - { - // Arrange - var issueHandle = CreateIssueHandle(111, new Dictionary + [DataTestMethod] + [DataRow(SuppressionState.Suppressed, true)] + [DataRow(SuppressionState.NotApplicable, false)] + [DataRow(SuppressionState.Active, false)] + public void TryGetRuleIdAndSuppressionStateFromSelectedRow_NoSuppressionState_ReturnsIsNotSuppressed(SuppressionState suppressionState, bool expectedSuppression) + { + var issueHandle = CreateIssueHandle(111, + new Dictionary { { StandardTableKeyNames.BuildTool, "SonarLint" }, { StandardTableKeyNames.ErrorCode, "cpp:S222" }, - { StandardTableKeyNames.SuppressionState, suppressionState }, + { StandardTableKeyNames.SuppressionState, suppressionState } }); + MockErrorList(issueHandle); - var errorList = CreateErrorList(issueHandle); - var serviceProvider = CreateServiceOperation(errorList); - - // Act - var testSubject = new ErrorListHelper(serviceProvider); - bool result = testSubject.TryGetRuleIdAndSuppressionStateFromSelectedRow(out var ruleId, out var isSuppressed); + var result = testSubject.TryGetRuleIdAndSuppressionStateFromSelectedRow(out _, out var isSuppressed); - // Assert - result.Should().BeTrue(); - isSuppressed.Should().Be(expectedSuppression); - } + result.Should().BeTrue(); + isSuppressed.Should().Be(expectedSuppression); + } - [TestMethod] - public void TryGetFilterableIssue_SonarIssue_IssueReturned() - { - var issueMock = Mock.Of(); - var issueHandle = CreateIssueHandle(111, new Dictionary + [TestMethod] + public void TryGetFilterableIssue_SonarIssue_IssueReturned() + { + var issueHandle = CreateIssueHandle(111, + new Dictionary { { StandardTableKeyNames.BuildTool, "SonarLint" }, - { StandardTableKeyNames.ErrorCode, "javascript:S333"}, + { StandardTableKeyNames.ErrorCode, "javascript:S333" }, { SonarLintTableControlConstants.IssueVizColumnName, issueMock } }); - var errorList = CreateErrorList(issueHandle); - var serviceProvider = CreateServiceOperation(errorList); - var testSubject = new ErrorListHelper(serviceProvider); + MockErrorList(issueHandle); - bool result = testSubject.TryGetFilterableIssue(issueHandle, out var issue); + var result = testSubject.TryGetFilterableIssue(issueHandle, out var issue); - result.Should().BeTrue(); - issue.Should().BeSameAs(issueMock); - } + result.Should().BeTrue(); + issue.Should().BeSameAs(issueMock); + } - [TestMethod] - public void TryGetFilterableIssue_NoAnalysisIssue_IssueNotReturned() - { - var issueHandle = CreateIssueHandle(111, new Dictionary + [TestMethod] + public void TryGetFilterableIssue_NoAnalysisIssue_IssueNotReturned() + { + var issueHandle = CreateIssueHandle(111, + new Dictionary { { StandardTableKeyNames.BuildTool, "SonarLint" }, - { StandardTableKeyNames.ErrorCode, "javascript:S333"}, + { StandardTableKeyNames.ErrorCode, "javascript:S333" }, { SonarLintTableControlConstants.IssueVizColumnName, null } }); - var errorList = CreateErrorList(issueHandle); - var serviceProvider = CreateServiceOperation(errorList); - - var testSubject = new ErrorListHelper(serviceProvider); - var result = testSubject.TryGetFilterableIssue(issueHandle,out _); + MockErrorList(issueHandle); - result.Should().BeFalse(); - } + var result = testSubject.TryGetFilterableIssue(issueHandle, out _); - private IVsUIServiceOperation CreateServiceOperation(IErrorList svcToPassToCallback) - { - var serviceOp = new Mock(); + result.Should().BeFalse(); + } - // Set up the mock to invoke the operation with the supplied VS service - serviceOp.Setup(x => x.Execute(It.IsAny>())) - .Returns>(op => op(svcToPassToCallback)); + private void MockErrorList(params ITableEntryHandle[] entries) + { + var errorList = CreateErrorList(entries); + // Set up the mock to invoke the operation with the supplied VS service + vsUiServiceOperation.Execute(Arg.Any>()) + .Returns(callInfo => + { + var func = callInfo.Arg>(); + return func(errorList); + }); + } - return serviceOp.Object; - } + private static IErrorList CreateErrorList(params ITableEntryHandle[] entries) + { + var mockWpfTable = Substitute.For(); + mockWpfTable.SelectedEntries.Returns(entries); - private static IErrorList CreateErrorList(params ITableEntryHandle[] entries) - { - var mockWpfTable = new Mock(); - mockWpfTable.Setup(x => x.SelectedEntries).Returns(entries); + var mockErrorList = Substitute.For(); + mockErrorList.TableControl.Returns(mockWpfTable); + return mockErrorList; + } - var mockErrorList = new Mock(); - mockErrorList.Setup(x => x.TableControl).Returns(mockWpfTable.Object); - return mockErrorList.Object; - } + private static ITableEntryHandle CreateIssueHandle(int index, IDictionary issueProperties) + { + // Snapshots would normally have multiple versions; each version would have a unique + // index, with a corresponding handle. + // Here, just create a dummy snapshot with a single version using the specified index + var issueSnapshot = (ITableEntriesSnapshot)new DummySnapshot { Index = index, Properties = issueProperties }; - private static ITableEntryHandle CreateIssueHandle(int index, IDictionary issueProperties) + var mockHandle = Substitute.For(); + mockHandle.TryGetSnapshot(out _, out _).Returns(callInfo => { - // Snapshots would normally have multiple versions; each version would have a unique - // index, with a corresponding handle. - // Here, just create a dummy snapshot with a single version using the specified index - var issueSnapshot = (ITableEntriesSnapshot)new DummySnapshot - { - Index = index, - Properties = issueProperties - }; - - var mockHandle = new Mock(); - mockHandle.Setup(x => x.TryGetSnapshot(out issueSnapshot, out index)).Returns(true); - return mockHandle.Object; - } + callInfo[0] = issueSnapshot; + callInfo[1] = index; + return true; + }); + return mockHandle; + } - #region Helper classes + #region Helper classes - private sealed class DummySnapshot : ITableEntriesSnapshot - { - public int Index { get; set; } - public IDictionary Properties { get; set; } + private sealed class DummySnapshot : ITableEntriesSnapshot + { + public int Index { get; set; } + public IDictionary Properties { get; set; } - #region ITableEntriesSnapshot methods + #region ITableEntriesSnapshot methods - public int Count => throw new NotImplementedException(); - public int VersionNumber => throw new NotImplementedException(); + public int Count => throw new NotImplementedException(); + public int VersionNumber => throw new NotImplementedException(); - public void Dispose() => throw new NotImplementedException(); + public void Dispose() => throw new NotImplementedException(); - public int IndexOf(int currentIndex, ITableEntriesSnapshot newSnapshot) => throw new NotImplementedException(); + public int IndexOf(int currentIndex, ITableEntriesSnapshot newSnapshot) => throw new NotImplementedException(); - public void StartCaching() => throw new NotImplementedException(); + public void StartCaching() => throw new NotImplementedException(); - public void StopCaching() => throw new NotImplementedException(); + public void StopCaching() => throw new NotImplementedException(); - public bool TryGetValue(int index, string keyName, out object content) + public bool TryGetValue(int index, string keyName, out object content) + { + if (index == Index) { - if (index == Index) - { - return Properties.TryGetValue(keyName, out content); - } - content = null; - return false; + return Properties.TryGetValue(keyName, out content); } - - #endregion ITableEntriesSnapshot methods + content = null; + return false; } - #endregion Helper classes + #endregion ITableEntriesSnapshot methods } + + #endregion Helper classes } diff --git a/src/Infrastructure.VS/ErrorListHelper.cs b/src/Infrastructure.VS/ErrorListHelper.cs index 4beb5f0b80..f31c474f92 100644 --- a/src/Infrastructure.VS/ErrorListHelper.cs +++ b/src/Infrastructure.VS/ErrorListHelper.cs @@ -26,209 +26,199 @@ using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Suppressions; -namespace SonarLint.VisualStudio.Infrastructure.VS +namespace SonarLint.VisualStudio.Infrastructure.VS; + +[Export(typeof(IErrorListHelper))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +public class ErrorListHelper(IVsUIServiceOperation vSServiceOperation) : IErrorListHelper { - [Export(typeof(IErrorListHelper))] - [PartCreationPolicy(CreationPolicy.Shared)] - public class ErrorListHelper : IErrorListHelper + public bool TryGetRuleIdFromSelectedRow(out SonarCompositeRuleId ruleId) { - private readonly IVsUIServiceOperation vSServiceOperation; + SonarCompositeRuleId ruleIdOut = null; + var result = vSServiceOperation.Execute(errorList => + TryGetSelectedTableEntry(errorList, out var handle) && TryGetRuleId(handle, out ruleIdOut)); - [ImportingConstructor] - public ErrorListHelper(IVsUIServiceOperation vSServiceOperation) - { - this.vSServiceOperation = vSServiceOperation; - } - - public bool TryGetRuleIdFromSelectedRow(out SonarCompositeRuleId ruleId) - { - SonarCompositeRuleId ruleIdOut = null; - var result = vSServiceOperation.Execute(errorList => - TryGetSelectedTableEntry(errorList, out var handle) && TryGetRuleId(handle, out ruleIdOut)); + ruleId = ruleIdOut; - ruleId = ruleIdOut; + return result; + } - return result; + public bool TryGetRuleId(ITableEntryHandle handle, out SonarCompositeRuleId ruleId) + { + ruleId = null; + if (!handle.TryGetSnapshot(out var snapshot, out var index)) + { + return false; } - public bool TryGetRuleId(ITableEntryHandle handle, out SonarCompositeRuleId ruleId) + var errorCode = FindErrorCodeForEntry(snapshot, index); + return SonarCompositeRuleId.TryParse(errorCode, out ruleId); + } + + public bool TryGetRuleIdAndSuppressionStateFromSelectedRow(out SonarCompositeRuleId ruleId, out bool isSuppressed) + { + SonarCompositeRuleId ruleIdOut = null; + var isSuppressedOut = false; + var result = vSServiceOperation.Execute(errorList => { - ruleId = null; - if (!handle.TryGetSnapshot(out var snapshot, out var index)) + if (!TryGetSelectedTableEntry(errorList, out var handle) || !TryGetRuleId(handle, out ruleIdOut)) { return false; } - var errorCode = FindErrorCodeForEntry(snapshot, index); - return SonarCompositeRuleId.TryParse(errorCode, out ruleId); - } + isSuppressedOut = IsSuppressed(handle); + return true; + }); - public bool TryGetRuleIdAndSuppressionStateFromSelectedRow(out SonarCompositeRuleId ruleId, out bool isSuppressed) - { - SonarCompositeRuleId ruleIdOut = null; - var isSuppressedOut = false; - var result = vSServiceOperation.Execute(errorList => - { - if (!TryGetSelectedTableEntry(errorList, out var handle) || !TryGetRuleId(handle, out ruleIdOut)) - { - return false; - } + ruleId = ruleIdOut; + isSuppressed = isSuppressedOut; - isSuppressedOut = IsSuppressed(handle); - return true; - }); + return result; + } - ruleId = ruleIdOut; - isSuppressed = isSuppressedOut; + public bool TryGetIssueFromSelectedRow(out IFilterableIssue issue) + { + IFilterableIssue issueOut = null; + var result = vSServiceOperation.Execute( + errorList => TryGetSelectedTableEntry(errorList, out var handle) && TryGetFilterableIssue(handle, out issueOut)); - return result; - } + issue = issueOut; - public bool TryGetIssueFromSelectedRow(out IFilterableIssue issue) - { - IFilterableIssue issueOut = null; - var result = vSServiceOperation.Execute( - errorList => TryGetSelectedTableEntry(errorList, out var handle) && TryGetFilterableIssue(handle, out issueOut)); + return result; + } + + public bool TryGetFilterableIssue(ITableEntryHandle handle, out IFilterableIssue issue) + { + IFilterableIssue issueOut = null; + var result = vSServiceOperation.Execute( + _ => handle.TryGetSnapshot(out var snapshot, out var index) + && TryGetValue(snapshot, index, SonarLintTableControlConstants.IssueVizColumnName, out issueOut)); - issue = issueOut; + issue = issueOut; - return result; - } + return result; + } + + public bool TryGetRoslynIssueFromSelectedRow(out IFilterableRoslynIssue filterableRoslynIssue) + { + IFilterableRoslynIssue outIssue = null; - public bool TryGetFilterableIssue(ITableEntryHandle handle, out IFilterableIssue issue) + var result = vSServiceOperation.Execute(errorList => { - IFilterableIssue issueOut = null; - var result = vSServiceOperation.Execute( - _ => handle.TryGetSnapshot(out var snapshot, out var index) - && TryGetValue(snapshot, index, SonarLintTableControlConstants.IssueVizColumnName, out issueOut)); + string errorCode; + if (TryGetSelectedSnapshotAndIndex(errorList, out var snapshot, out var index) + && (errorCode = FindErrorCodeForEntry(snapshot, index)) != null + && TryGetValue(snapshot, index, StandardTableKeyNames.DocumentName, out string filePath) + && TryGetValue(snapshot, index, StandardTableKeyNames.Line, out int line) + && TryGetValue(snapshot, index, StandardTableKeyNames.Column, out int column)) + { + outIssue = new FilterableRoslynIssue(errorCode, filePath, line + 1, column + 1 /* error list issues are 0-based and we use 1-based line & column numbers */); + } - issue = issueOut; + return outIssue != null; + }); - return result; - } + filterableRoslynIssue = outIssue; - public bool TryGetRoslynIssueFromSelectedRow(out IFilterableRoslynIssue filterableRoslynIssue) - { - IFilterableRoslynIssue outIssue = null; + return result; + } - var result = vSServiceOperation.Execute(errorList => - { - string errorCode; - if (TryGetSelectedSnapshotAndIndex(errorList, out var snapshot, out var index) - && (errorCode = FindErrorCodeForEntry(snapshot, index)) != null - && TryGetValue(snapshot, index, StandardTableKeyNames.DocumentName, out string filePath) - && TryGetValue(snapshot, index, StandardTableKeyNames.Line, out int line) - && TryGetValue(snapshot, index, StandardTableKeyNames.Column, out int column)) - { - outIssue = new FilterableRoslynIssue(errorCode, filePath, line + 1, column + 1 /* error list issues are 0-based and we use 1-based line & column numbers */); - } - - return outIssue != null; - }); - - filterableRoslynIssue = outIssue; - - return result; - } + private static bool IsSuppressed(ITableEntryHandle handle) => + handle.TryGetSnapshot(out var snapshot, out var index) + && TryGetValue(snapshot, index, StandardTableKeyNames.SuppressionState, out SuppressionState suppressionState) + && suppressionState == SuppressionState.Suppressed; - private static bool IsSuppressed(ITableEntryHandle handle) + private static string FindErrorCodeForEntry(ITableEntriesSnapshot snapshot, int index) + { + if (!TryGetValue(snapshot, index, StandardTableKeyNames.ErrorCode, out string errorCode)) { - return handle.TryGetSnapshot(out var snapshot, out var index) - && TryGetValue(snapshot, index, StandardTableKeyNames.SuppressionState, out SuppressionState suppressionState) - && suppressionState == SuppressionState.Suppressed; + return null; } - private static string FindErrorCodeForEntry(ITableEntriesSnapshot snapshot, int index) + if (TryGetValue(snapshot, index, StandardTableKeyNames.BuildTool, out string buildTool)) { - if (!TryGetValue(snapshot, index, StandardTableKeyNames.ErrorCode, out string errorCode)) + // For CSharp and VisualBasic the buildTool returns the name of the analyzer package. + // The prefix is required for roslyn languages as the error code is in style "S111" meaning + // unlike other languages it has no repository prefix. + return buildTool switch { - return null; - } + "SonarAnalyzer.CSharp" => $"{SonarRuleRepoKeys.CSharpRules}:{errorCode}", + "SonarAnalyzer.VisualBasic" => $"{SonarRuleRepoKeys.VBNetRules}:{errorCode}", + "SonarLint" => errorCode, + _ => null + }; + } - if (TryGetValue(snapshot, index, StandardTableKeyNames.BuildTool, out string buildTool)) + if (TryGetValue(snapshot, index, StandardTableKeyNames.HelpLink, out string helpLink)) + { + if (helpLink.Contains("rules.sonarsource.com/csharp/")) { - // For CSharp and VisualBasic the buildTool returns the name of the analyzer package. - // The prefix is required for roslyn languages as the error code is in style "S111" meaning - // unlike other languages it has no repository prefix. - return buildTool switch - { - "SonarAnalyzer.CSharp" => $"{SonarRuleRepoKeys.CSharpRules}:{errorCode}", - "SonarAnalyzer.VisualBasic" => $"{SonarRuleRepoKeys.VBNetRules}:{errorCode}", - "SonarLint" => errorCode, - _ => null - }; + return $"{SonarRuleRepoKeys.CSharpRules}:{errorCode}"; } - if (TryGetValue(snapshot, index, StandardTableKeyNames.HelpLink, out string helpLink)) + if (helpLink.Contains("rules.sonarsource.com/vbnet/")) { - if (helpLink.Contains("rules.sonarsource.com/csharp/")) - { - return $"{SonarRuleRepoKeys.CSharpRules}:{errorCode}"; - } - - if (helpLink.Contains("rules.sonarsource.com/vbnet/")) - { - return $"{SonarRuleRepoKeys.VBNetRules}:{errorCode}"; - } + return $"{SonarRuleRepoKeys.VBNetRules}:{errorCode}"; } - - return null; } - private static bool TryGetSelectedSnapshotAndIndex(IErrorList errorList, out ITableEntriesSnapshot snapshot, out int index) - { - snapshot = default; - index = default; + return null; + } - return TryGetSelectedTableEntry(errorList, out var handle) && handle.TryGetSnapshot(out snapshot, out index); - } + private static bool TryGetSelectedSnapshotAndIndex(IErrorList errorList, out ITableEntriesSnapshot snapshot, out int index) + { + snapshot = default; + index = default; - private static bool TryGetSelectedTableEntry(IErrorList errorList, out ITableEntryHandle handle) - { - handle = null; + return TryGetSelectedTableEntry(errorList, out var handle) && handle.TryGetSnapshot(out snapshot, out index); + } - var selectedItems = errorList?.TableControl?.SelectedEntries; + private static bool TryGetSelectedTableEntry(IErrorList errorList, out ITableEntryHandle handle) + { + handle = null; - if (selectedItems == null) - { - return false; - } + var selectedItems = errorList?.TableControl?.SelectedEntries; - foreach (var tableEntryHandle in selectedItems) - { - if (handle != null) - { - return false; // more than one selected is not supported - } + if (selectedItems == null) + { + return false; + } - handle = tableEntryHandle; + foreach (var tableEntryHandle in selectedItems) + { + if (handle != null) + { + return false; // more than one selected is not supported } - return true; + handle = tableEntryHandle; } - private static bool TryGetValue( - ITableEntriesSnapshot snapshot, - int index, - string columnName, - out T value) - { - value = default; + return true; + } - try - { - if (!snapshot.TryGetValue(index, columnName, out var objValue) || objValue == null) - { - return false; - } + private static bool TryGetValue( + ITableEntriesSnapshot snapshot, + int index, + string columnName, + out T value) + { + value = default; - value = (T)objValue; - return true; - } - catch (InvalidCastException) + try + { + if (!snapshot.TryGetValue(index, columnName, out var objValue) || objValue == null) { return false; } + + value = (T)objValue; + return true; + } + catch (InvalidCastException) + { + return false; } } } diff --git a/src/IssueViz.UnitTests/Models/AnalysisIssueVisualizationTests.cs b/src/IssueViz.UnitTests/Models/AnalysisIssueVisualizationTests.cs index 93dab60f90..de2516e8dd 100644 --- a/src/IssueViz.UnitTests/Models/AnalysisIssueVisualizationTests.cs +++ b/src/IssueViz.UnitTests/Models/AnalysisIssueVisualizationTests.cs @@ -18,248 +18,192 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using System.ComponentModel; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.Text; -using Moq; using SonarLint.VisualStudio.Core.Analysis; using SonarLint.VisualStudio.Core.Suppressions; using SonarLint.VisualStudio.IssueVisualization.Models; -namespace SonarLint.VisualStudio.IssueVisualization.UnitTests.Models +namespace SonarLint.VisualStudio.IssueVisualization.UnitTests.Models; + +[TestClass] +public class AnalysisIssueVisualizationTests { - [TestClass] - public class AnalysisIssueVisualizationTests - { - [TestMethod] - public void Ctor_StepNumberIsZero() - { - var testSubject = CreateTestSubject(); - - testSubject.StepNumber.Should().Be(0); - } - - [TestMethod] - public void Ctor_InitialFilePathIsTakenFromIssue() - { - var testSubject = CreateTestSubject(filePath: "test path"); - - testSubject.CurrentFilePath.Should().Be("test path"); - } - - [TestMethod] - public void Ctor_NullSpan_InitialSpanIsSetToGivenValue() - { - var testSubject = CreateTestSubject(span: null); - - testSubject.Span.Should().BeNull(); - } - - [TestMethod] - public void Ctor_EmptySpan_InitialSpanIsSetToGivenValue() - { - var span = new SnapshotSpan(); - var testSubject = CreateTestSubject(span: span); - - testSubject.Span.Should().Be(span); - } - - [TestMethod] - public void Ctor_NonEmptySpan_InitialSpanIsSetToGivenValue() - { - var span = CreateSpan(); - var testSubject = CreateTestSubject(span: span); - - testSubject.Span.Should().Be(span); - } - - [TestMethod] - public void Location_ReturnsUnderlyingIssueLocation() - { - var testSubject = CreateTestSubject(); - testSubject.Location.Should().Be(testSubject.Issue.PrimaryLocation); - } - - [TestMethod] - public void SetCurrentFilePath_FilePathIsNull_SpanIsInvalidated() - { - // Arrange - var oldFilePath = "oldpath.txt"; - var oldSpan = CreateSpan(); - - var testSubject = CreateTestSubject(oldFilePath, oldSpan); - testSubject.Span.Should().Be(oldSpan); - testSubject.CurrentFilePath.Should().Be(oldFilePath); - - var propertyChangedEventHandler = new Mock(); - testSubject.PropertyChanged += propertyChangedEventHandler.Object; + private readonly SnapshotSpan emptySpan = new(); + private readonly string filePath = "filePath.txt"; - // Act - testSubject.CurrentFilePath = null; + private IAnalysisIssue issue = Substitute.For(); + private AnalysisIssueVisualization issueVisualizationWithEmptySpan; + private AnalysisIssueVisualization issueVisualizationWithNoSpan; + private AnalysisIssueVisualization issueVisualizationWithNotEmptySpan; + private SnapshotSpan notEmptySpan; - // Assert - testSubject.Span.Value.IsEmpty.Should().BeTrue(); - testSubject.CurrentFilePath.Should().BeNull(); + [TestInitialize] + public void TestInitialize() + { + notEmptySpan = CreateSpan(); + issue = Substitute.For(); + MockAnalysisIssue(); - VerifyPropertyChangedRaised(propertyChangedEventHandler, nameof(testSubject.CurrentFilePath)); - VerifyPropertyChangedRaised(propertyChangedEventHandler, nameof(testSubject.Span)); - propertyChangedEventHandler.VerifyNoOtherCalls(); - } + issueVisualizationWithNoSpan = new AnalysisIssueVisualization(null, issue, null, null); + issueVisualizationWithEmptySpan = new AnalysisIssueVisualization(null, issue, emptySpan, null); + issueVisualizationWithNotEmptySpan = new AnalysisIssueVisualization(null, issue, notEmptySpan, null); + } - [TestMethod] - public void SetCurrentFilePath_FilePathIsNotNull_SpanNotChanged() - { - // Arrange - var oldFilePath = "oldpath.txt"; - var oldSpan = CreateSpan(); + [TestMethod] + public void Ctor_StepNumberIsZero() => issueVisualizationWithNoSpan.StepNumber.Should().Be(0); - var testSubject = CreateTestSubject(oldFilePath, oldSpan); - testSubject.Span.Should().Be(oldSpan); - testSubject.CurrentFilePath.Should().Be(oldFilePath); + [TestMethod] + public void Ctor_InitialFilePathIsTakenFromIssue() => issueVisualizationWithNoSpan.CurrentFilePath.Should().Be(filePath); - var propertyChangedEventHandler = new Mock(); - testSubject.PropertyChanged += propertyChangedEventHandler.Object; + [TestMethod] + public void Ctor_NullSpan_InitialSpanIsSetToGivenValue() => issueVisualizationWithNoSpan.Span.Should().BeNull(); - // Act - testSubject.CurrentFilePath = "newpath.txt"; + [TestMethod] + public void Ctor_EmptySpan_InitialSpanIsSetToGivenValue() => issueVisualizationWithEmptySpan.Span.Should().Be(emptySpan); - // Assert - testSubject.Span.Should().Be(oldSpan); - testSubject.CurrentFilePath.Should().Be("newpath.txt"); + [TestMethod] + public void Ctor_NonEmptySpan_InitialSpanIsSetToGivenValue() => issueVisualizationWithNotEmptySpan.Span.Should().Be(notEmptySpan); - VerifyPropertyChangedRaised(propertyChangedEventHandler, nameof(testSubject.CurrentFilePath)); - VerifyPropertyChangedNotRaised(propertyChangedEventHandler, nameof(testSubject.Span)); - propertyChangedEventHandler.VerifyNoOtherCalls(); - } + [TestMethod] + public void Location_ReturnsUnderlyingIssueLocation() => issueVisualizationWithEmptySpan.Location.Should().Be(issueVisualizationWithEmptySpan.Issue.PrimaryLocation); - [TestMethod] - public void SetCurrentFilePath_NoSubscribers_NoException() - { - var testSubject = CreateTestSubject(); + [TestMethod] + public void SetCurrentFilePath_FilePathIsNull_SpanIsInvalidated() + { + VerifySpanAndLocationCorrect(filePath); + var propertyChangedEventHandler = MockSubscriberToPropertyChanged(); - Action act = () => testSubject.CurrentFilePath = "new path"; - act.Should().NotThrow(); + issueVisualizationWithNotEmptySpan.CurrentFilePath = null; - testSubject.CurrentFilePath.Should().Be("new path"); - } + issueVisualizationWithNotEmptySpan.Span.Value.IsEmpty.Should().BeTrue(); + issueVisualizationWithNotEmptySpan.CurrentFilePath.Should().BeNull(); + VerifyPropertyChangedRaised(propertyChangedEventHandler, issueVisualizationWithNotEmptySpan, nameof(issueVisualizationWithNotEmptySpan.CurrentFilePath)); + VerifyPropertyChangedRaised(propertyChangedEventHandler, issueVisualizationWithNotEmptySpan, nameof(issueVisualizationWithNotEmptySpan.Span)); + propertyChangedEventHandler.ReceivedCalls().Count().Should().Be(2); + } - [TestMethod] - public void SetCurrentFilePath_HasSubscribers_NotifiesSubscribers() - { - var propertyChangedEventHandler = new Mock(); + [TestMethod] + public void SetCurrentFilePath_FilePathIsNotNull_SpanNotChanged() + { + VerifySpanAndLocationCorrect(filePath); + var propertyChangedEventHandler = MockSubscriberToPropertyChanged(); - var testSubject = CreateTestSubject(); - testSubject.PropertyChanged += propertyChangedEventHandler.Object; + issueVisualizationWithNotEmptySpan.CurrentFilePath = "newpath.txt"; - testSubject.CurrentFilePath = "new path"; + issueVisualizationWithNotEmptySpan.Span.Should().Be(notEmptySpan); + issueVisualizationWithNotEmptySpan.CurrentFilePath.Should().Be("newpath.txt"); + VerifyPropertyChangedRaised(propertyChangedEventHandler, issueVisualizationWithNotEmptySpan, nameof(issueVisualizationWithNotEmptySpan.CurrentFilePath)); + VerifyPropertyChangedNotRaised(propertyChangedEventHandler, nameof(issueVisualizationWithNotEmptySpan.Span)); + propertyChangedEventHandler.ReceivedCalls().Count().Should().Be(1); + } - VerifyPropertyChangedRaised(propertyChangedEventHandler, nameof(testSubject.CurrentFilePath)); - propertyChangedEventHandler.VerifyNoOtherCalls(); - - testSubject.CurrentFilePath.Should().Be("new path"); - } - - [TestMethod] - public void SetSpan_NoSubscribers_NoException() - { - var newSpan = CreateSpan(); - var testSubject = CreateTestSubject(); - - Action act = () => testSubject.Span = newSpan; - act.Should().NotThrow(); - - testSubject.Span.Should().Be(newSpan); - } - - [TestMethod] - public void SetSpan_HasSubscribers_NotifiesSubscribers() - { - var newSpan = CreateSpan(); - var propertyChangedEventHandler = new Mock(); + [TestMethod] + public void SetCurrentFilePath_NoSubscribers_NoException() + { + Action act = () => issueVisualizationWithNotEmptySpan.CurrentFilePath = "new path"; + act.Should().NotThrow(); - var testSubject = CreateTestSubject(); - testSubject.PropertyChanged += propertyChangedEventHandler.Object; + issueVisualizationWithNotEmptySpan.CurrentFilePath.Should().Be("new path"); + } - testSubject.Span = newSpan; + [TestMethod] + public void SetCurrentFilePath_HasSubscribers_NotifiesSubscribers() + { + var propertyChangedEventHandler = MockSubscriberToPropertyChanged(); - VerifyPropertyChangedRaised(propertyChangedEventHandler, nameof(testSubject.Span)); - propertyChangedEventHandler.VerifyNoOtherCalls(); + issueVisualizationWithNotEmptySpan.CurrentFilePath = "new path"; - testSubject.Span.Should().Be(newSpan); - } + VerifyPropertyChangedRaised(propertyChangedEventHandler, issueVisualizationWithNotEmptySpan, nameof(issueVisualizationWithNotEmptySpan.CurrentFilePath)); + propertyChangedEventHandler.ReceivedCalls().Count().Should().Be(1); + issueVisualizationWithNotEmptySpan.CurrentFilePath.Should().Be("new path"); + } - [TestMethod] - public void SetIsSuppressed_HasSubscribers_VerifyRaised() - { - var testSubject = CreateTestSubject(); + [TestMethod] + public void SetSpan_NoSubscribers_NoException() + { + var newSpan = CreateSpan(); - testSubject.IsSuppressed.Should().BeFalse(); + Action act = () => issueVisualizationWithNotEmptySpan.Span = newSpan; + act.Should().NotThrow(); - var propertyChangedEventHandler = new Mock(); - testSubject.PropertyChanged += propertyChangedEventHandler.Object; + issueVisualizationWithNotEmptySpan.Span.Should().Be(newSpan); + } - testSubject.IsSuppressed = true; + [TestMethod] + public void SetSpan_HasSubscribers_NotifiesSubscribers() + { + var newSpan = CreateSpan(); + var propertyChangedEventHandler = MockSubscriberToPropertyChanged(); - VerifyPropertyChangedRaised(propertyChangedEventHandler, nameof(testSubject.IsSuppressed)); - propertyChangedEventHandler.VerifyNoOtherCalls(); + issueVisualizationWithNotEmptySpan.Span = newSpan; - testSubject.IsSuppressed.Should().BeTrue(); - } + VerifyPropertyChangedRaised(propertyChangedEventHandler, issueVisualizationWithNotEmptySpan, nameof(issueVisualizationWithNotEmptySpan.Span)); + propertyChangedEventHandler.ReceivedCalls().Count().Should().Be(1); + issueVisualizationWithNotEmptySpan.Span.Should().Be(newSpan); + } - [TestMethod] - public void IsFilterable() - { - var id = Guid.NewGuid(); - var issueMock = new Mock(); - issueMock.SetupGet(x => x.Id).Returns(id); - issueMock.SetupGet(x => x.RuleKey).Returns("my key"); - issueMock.SetupGet(x => x.PrimaryLocation.FilePath).Returns("x:\\aaa.foo"); - issueMock.SetupGet(x => x.PrimaryLocation.TextRange.StartLine).Returns(999); - issueMock.SetupGet(x => x.PrimaryLocation.TextRange.LineHash).Returns("hash"); + [TestMethod] + public void SetIsSuppressed_HasSubscribers_VerifyRaised() + { + issueVisualizationWithNotEmptySpan.IsSuppressed.Should().BeFalse(); + var propertyChangedEventHandler = MockSubscriberToPropertyChanged(); - var testSubject = new AnalysisIssueVisualization(null, issueMock.Object, new SnapshotSpan(), null); + issueVisualizationWithNotEmptySpan.IsSuppressed = true; - testSubject.Should().BeAssignableTo(); + VerifyPropertyChangedRaised(propertyChangedEventHandler, issueVisualizationWithNotEmptySpan, nameof(issueVisualizationWithNotEmptySpan.IsSuppressed)); + propertyChangedEventHandler.ReceivedCalls().Count().Should().Be(1); + issueVisualizationWithNotEmptySpan.IsSuppressed.Should().BeTrue(); + } - var filterable = (IFilterableIssue)testSubject; + [TestMethod] + public void IsFilterable() + { + issueVisualizationWithEmptySpan.Should().BeAssignableTo(); + + var filterable = (IFilterableIssue)issueVisualizationWithEmptySpan; + filterable.IssueId.Should().Be(issue.Id); + filterable.RuleId.Should().Be(issue.RuleKey); + filterable.FilePath.Should().Be(issue.PrimaryLocation.FilePath); + filterable.StartLine.Should().Be(issue.PrimaryLocation.TextRange.StartLine); + filterable.LineHash.Should().Be(issue.PrimaryLocation.TextRange.LineHash); + } - filterable.IssueId.Should().Be(id); - filterable.RuleId.Should().Be(issueMock.Object.RuleKey); - filterable.FilePath.Should().Be(issueMock.Object.PrimaryLocation.FilePath); - filterable.StartLine.Should().Be(issueMock.Object.PrimaryLocation.TextRange.StartLine); - filterable.LineHash.Should().Be(issueMock.Object.PrimaryLocation.TextRange.LineHash); - } + private void MockAnalysisIssue() + { + var id = Guid.NewGuid(); + issue.Id.Returns(id); + issue.RuleKey.Returns("my key"); + issue.PrimaryLocation.FilePath.Returns("x:\\aaa.foo"); + issue.PrimaryLocation.TextRange.StartLine.Returns(999); + issue.PrimaryLocation.TextRange.LineHash.Returns("hash"); + issue.PrimaryLocation.FilePath.Returns(filePath); + } - private SnapshotSpan CreateSpan() - { - var mockTextSnapshot = new Mock(); - mockTextSnapshot.SetupGet(x => x.Length).Returns(20); + private SnapshotSpan CreateSpan() + { + var mockTextSnapshot = Substitute.For(); + mockTextSnapshot.Length.Returns(20); - return new SnapshotSpan(mockTextSnapshot.Object, new Span(0, 10)); - } + return new SnapshotSpan(mockTextSnapshot, new Span(0, 10)); + } - private AnalysisIssueVisualization CreateTestSubject(string filePath = null, SnapshotSpan? span = null) - { - var issue = new Mock(); - issue.SetupGet(x => x.PrimaryLocation.FilePath).Returns(filePath); + private void VerifyPropertyChangedRaised(PropertyChangedEventHandler propertyChangedEventHandler, AnalysisIssueVisualization testSubject, string propertyName) => + propertyChangedEventHandler.Received().Invoke(testSubject, Arg.Is(x => x.PropertyName == propertyName)); - return new AnalysisIssueVisualization(null, issue.Object, span, null); - } + private void VerifyPropertyChangedNotRaised(PropertyChangedEventHandler propertyChangedEventHandler, string propertyName) => + propertyChangedEventHandler.DidNotReceive().Invoke(Arg.Any(), Arg.Is(x => x.PropertyName == propertyName)); - private void VerifyPropertyChangedRaised(Mock propertyChangedEventHandler, string propertyName) - { - propertyChangedEventHandler.Verify(x => - x(It.IsAny(), It.Is((PropertyChangedEventArgs e) => e.PropertyName == propertyName)), - Times.Once); - } + private PropertyChangedEventHandler MockSubscriberToPropertyChanged() + { + var propertyChangedEventHandler = Substitute.For(); + issueVisualizationWithNotEmptySpan.PropertyChanged += propertyChangedEventHandler; + return propertyChangedEventHandler; + } - private void VerifyPropertyChangedNotRaised(Mock propertyChangedEventHandler, string propertyName) - { - propertyChangedEventHandler.Verify(x => - x(It.IsAny(), It.Is((PropertyChangedEventArgs e) => e.PropertyName == propertyName)), - Times.Never); - } + private void VerifySpanAndLocationCorrect(string oldFilePath) + { + issueVisualizationWithNotEmptySpan.Span.Should().Be(notEmptySpan); + issueVisualizationWithNotEmptySpan.CurrentFilePath.Should().Be(oldFilePath); } } diff --git a/src/IssueViz/Models/AnalysisIssueVisualization.cs b/src/IssueViz/Models/AnalysisIssueVisualization.cs index 7facf0c2e7..15b5f37caa 100644 --- a/src/IssueViz/Models/AnalysisIssueVisualization.cs +++ b/src/IssueViz/Models/AnalysisIssueVisualization.cs @@ -18,134 +18,126 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Collections.Generic; using System.ComponentModel; -using System.Linq; using System.Runtime.CompilerServices; using Microsoft.VisualStudio.Text; using SonarLint.VisualStudio.Core.Analysis; using SonarLint.VisualStudio.Core.Suppressions; -namespace SonarLint.VisualStudio.IssueVisualization.Models +namespace SonarLint.VisualStudio.IssueVisualization.Models; + +public interface IAnalysisIssueVisualization : IAnalysisIssueLocationVisualization, IFilterableIssue { - public interface IAnalysisIssueVisualization : IAnalysisIssueLocationVisualization, IFilterableIssue - { - IReadOnlyList Flows { get; } + IReadOnlyList Flows { get; } - IAnalysisIssueBase Issue { get; } + IAnalysisIssueBase Issue { get; } - IReadOnlyList QuickFixes { get; } + IReadOnlyList QuickFixes { get; } - bool IsSuppressed { get; set; } - } + bool IsSuppressed { get; set; } +} - internal class AnalysisIssueVisualization : IAnalysisIssueVisualization +internal class AnalysisIssueVisualization : IAnalysisIssueVisualization +{ + private static readonly SnapshotSpan EmptySpan = new(); + private string currentFilePath; + private bool isSuppressed; + private SnapshotSpan? span; + + public AnalysisIssueVisualization( + IReadOnlyList flows, + IAnalysisIssueBase issue, + SnapshotSpan? span, + IReadOnlyList quickFixes) { - private static readonly SnapshotSpan EmptySpan = new SnapshotSpan(); - private string currentFilePath; - private SnapshotSpan? span; - private bool isSuppressed; - - public AnalysisIssueVisualization(IReadOnlyList flows, - IAnalysisIssueBase issue, - SnapshotSpan? span, - IReadOnlyList quickFixes) - { - Flows = flows; - Issue = issue; - CurrentFilePath = issue.PrimaryLocation.FilePath; - Span = span; - QuickFixes = quickFixes; - } + Flows = flows; + Issue = issue; + CurrentFilePath = issue.PrimaryLocation.FilePath; + Span = span; + QuickFixes = quickFixes; + } - public IReadOnlyList Flows { get; } - public IReadOnlyList QuickFixes { get; } - public IAnalysisIssueBase Issue { get; } - public int StepNumber => 0; - public IAnalysisIssueLocation Location => Issue.PrimaryLocation; + public IReadOnlyList Flows { get; } + public IReadOnlyList QuickFixes { get; } + public IAnalysisIssueBase Issue { get; } + public int StepNumber => 0; + public IAnalysisIssueLocation Location => Issue.PrimaryLocation; - public SnapshotSpan? Span + public SnapshotSpan? Span + { + get => span; + set { - get => span; - set - { - span = value; - NotifyPropertyChanged(); - } + span = value; + NotifyPropertyChanged(); } + } - public bool IsSuppressed + public bool IsSuppressed + { + get => isSuppressed; + set { - get => isSuppressed; - set - { - isSuppressed = value; - NotifyPropertyChanged(); - } + isSuppressed = value; + NotifyPropertyChanged(); } + } - public string CurrentFilePath + public string CurrentFilePath + { + get => currentFilePath; + set { - get => currentFilePath; - set + currentFilePath = value; + + if (string.IsNullOrEmpty(currentFilePath)) { - currentFilePath = value; - - if (string.IsNullOrEmpty(currentFilePath)) - { - Span = EmptySpan; - } - - NotifyPropertyChanged(); + Span = EmptySpan; } + + NotifyPropertyChanged(); } + } - public event PropertyChangedEventHandler PropertyChanged; + public event PropertyChangedEventHandler PropertyChanged; - protected virtual void NotifyPropertyChanged([CallerMemberName] string propertyName = null) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } + public Guid? IssueId => Issue.Id; + string IFilterableIssue.RuleId => Issue.RuleKey; - public Guid? IssueId => Issue.Id; - string IFilterableIssue.RuleId => Issue.RuleKey; + string IFilterableIssue.FilePath => CurrentFilePath; - string IFilterableIssue.FilePath => CurrentFilePath; + string IFilterableIssue.LineHash => Issue.PrimaryLocation.TextRange.LineHash; - string IFilterableIssue.LineHash => Issue.PrimaryLocation.TextRange.LineHash; + int? IFilterableIssue.StartLine => Issue.PrimaryLocation.TextRange.StartLine; - int? IFilterableIssue.StartLine => Issue.PrimaryLocation.TextRange.StartLine; - } + protected virtual void NotifyPropertyChanged([CallerMemberName] string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); +} - public static class AnalysisIssueVisualizationExtensions +public static class AnalysisIssueVisualizationExtensions +{ + /// + /// Returns primary and all secondary locations of the given + /// + public static IEnumerable GetAllLocations(this IAnalysisIssueVisualization issueVisualization) { - /// - /// Returns primary and all secondary locations of the given - /// - public static IEnumerable GetAllLocations(this IAnalysisIssueVisualization issueVisualization) - { - var primaryLocation = issueVisualization; - var secondaryLocations = issueVisualization.GetSecondaryLocations(); + var primaryLocation = issueVisualization; + var secondaryLocations = issueVisualization.GetSecondaryLocations(); - var allLocations = new List {primaryLocation}; - allLocations.AddRange(secondaryLocations); + var allLocations = new List { primaryLocation }; + allLocations.AddRange(secondaryLocations); - return allLocations; - } - - /// - /// Returns all secondary locations of the given - /// - public static IEnumerable GetSecondaryLocations(this IAnalysisIssueVisualization issueVisualization) - { - var secondaryLocations = issueVisualization.Flows.SelectMany(x => x.Locations); + return allLocations; + } - return secondaryLocations; - } + /// + /// Returns all secondary locations of the given + /// + public static IEnumerable GetSecondaryLocations(this IAnalysisIssueVisualization issueVisualization) + { + var secondaryLocations = issueVisualization.Flows.SelectMany(x => x.Locations); - public static bool IsFileLevel(this IAnalysisIssueVisualization issueVisualization) - { - return issueVisualization.Issue.IsFileLevel(); - } + return secondaryLocations; } + + public static bool IsFileLevel(this IAnalysisIssueVisualization issueVisualization) => issueVisualization.Issue.IsFileLevel(); }