From 1fad501bf4c1720666abf5b168bafe1628f7f4cb Mon Sep 17 00:00:00 2001 From: Gabriela Trutan Date: Thu, 14 Nov 2024 12:09:08 +0100 Subject: [PATCH] SLVS-1598 Use getEffectiveIssueDetails for issues with SLCore id instead of getEffectiveRuleDetails (#5830) --- .../TestFilterableIssue.cs | 1 + src/Core/IEducation.cs | 5 +- .../Suppressions/FilterableRoslynIssue.cs | 4 + src/Core/Suppressions/IFilterableIssue.cs | 5 + src/Education.UnitTests/EducationTests.cs | 51 ++- .../SonarErrorListEventProcessorTests.cs | 25 +- .../Rule/RuleInfoConverterTests.cs | 430 ++++++++++++++++++ .../Rule/SLCoreRuleMetaDataProviderTests.cs | 398 ++++++---------- .../Controls/RuleHelpUserControl.xaml.cs | 2 +- src/Education/Education.cs | 13 +- .../ErrorList/SonarErrorListEventProcessor.cs | 3 +- src/Education/Rule/IRuleInfoConverter.cs | 100 ++++ src/Education/Rule/IRuleMetaDataProvider.cs | 9 +- .../Rule/SLCoreRuleMetaDataProvider.cs | 113 ++--- .../ErrorListHelperTests.cs | 38 ++ src/Infrastructure.VS/ErrorListHelper.cs | 24 +- src/Infrastructure.VS/IErrorListHelper.cs | 7 + .../HotspotsList/HotspotsControl.xaml | 1 + .../OpenInIDEHotspotsControl.xaml | 1 + .../Taint/TaintList/TaintIssuesControl.xaml | 1 + ...eToRuleDescriptionCommandConverterTests.cs | 39 +- .../NavigateToRuleDescriptionCommandTests.cs | 18 +- .../Models/AnalysisIssueVisualizationTests.cs | 3 + .../IssueVisualizationControl.xaml | 1 + .../NavigateToRuleDescriptionCommand.cs | 15 +- .../Models/AnalysisIssueVisualization.cs | 1 + .../RuleDescriptionConversionSmokeTest.cs | 1 + .../GetEffectiveIssueDetailsResponseTests.cs | 2 +- .../Issue/Models/EffectiveIssueDetailsDto.cs | 4 +- .../Rules/Models/EffectiveRuleDetailsDto.cs | 5 +- .../Service/Rules/Models/IRuleDetails.cs | 35 ++ 31 files changed, 968 insertions(+), 387 deletions(-) create mode 100644 src/Education.UnitTests/Rule/RuleInfoConverterTests.cs create mode 100644 src/Education/Rule/IRuleInfoConverter.cs create mode 100644 src/SLCore/Service/Rules/Models/IRuleDetails.cs diff --git a/src/ConnectedMode.UnitTests/TestFilterableIssue.cs b/src/ConnectedMode.UnitTests/TestFilterableIssue.cs index 845a0531ed..49a776772a 100644 --- a/src/ConnectedMode.UnitTests/TestFilterableIssue.cs +++ b/src/ConnectedMode.UnitTests/TestFilterableIssue.cs @@ -25,6 +25,7 @@ namespace SonarLint.VisualStudio.ConnectedMode.UnitTests; internal class TestFilterableIssue : IFilterableIssue { + public Guid? IssueId { get; set; } public string RuleId { get; set; } public string LineHash { get; set; } public int? StartLine { get; set; } diff --git a/src/Core/IEducation.cs b/src/Core/IEducation.cs index ac802a37da..2b848bd09f 100644 --- a/src/Core/IEducation.cs +++ b/src/Core/IEducation.cs @@ -18,6 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using SonarLint.VisualStudio.Core.Suppressions; + namespace SonarLint.VisualStudio.Core { /// @@ -32,6 +34,7 @@ public interface IEducation /// be displayed in the IDE. Otherwise, the rule help will be displayed in the /// browser i.e. at rules.sonarsource.com /// Key for the How to fix it Context acquired from a specific issue. Can be null. - void ShowRuleHelp(SonarCompositeRuleId ruleId, string issueContext); + /// The SlCore issue ID for which the rule help should be shown.s + void ShowRuleHelp(SonarCompositeRuleId ruleId, Guid? issueId, string issueContext); } } diff --git a/src/Core/Suppressions/FilterableRoslynIssue.cs b/src/Core/Suppressions/FilterableRoslynIssue.cs index e72f86399c..1bc9fcd74c 100644 --- a/src/Core/Suppressions/FilterableRoslynIssue.cs +++ b/src/Core/Suppressions/FilterableRoslynIssue.cs @@ -40,6 +40,10 @@ public FilterableRoslynIssue(string ruleId, string filePath, int startLine, int RoslynStartColumn = startColumn; } + /// + /// Always null, as this Id is specific to SlCore + /// + public Guid? IssueId => null; public string RuleId { get; } public string FilePath { get; } public int? StartLine => RoslynStartLine; diff --git a/src/Core/Suppressions/IFilterableIssue.cs b/src/Core/Suppressions/IFilterableIssue.cs index 34ef449f47..bb5e5256ef 100644 --- a/src/Core/Suppressions/IFilterableIssue.cs +++ b/src/Core/Suppressions/IFilterableIssue.cs @@ -26,6 +26,11 @@ namespace SonarLint.VisualStudio.Core.Suppressions /// public interface IFilterableIssue { + /// + /// The id of the issue that comes from SlCore + /// Nullable due to the fact that some issues do not come from SlCore (e.g. Roslyn) + /// + Guid? IssueId { get; } string RuleId { get; } string FilePath { get; } string LineHash { get; } diff --git a/src/Education.UnitTests/EducationTests.cs b/src/Education.UnitTests/EducationTests.cs index 8a0548916c..c049485d9e 100644 --- a/src/Education.UnitTests/EducationTests.cs +++ b/src/Education.UnitTests/EducationTests.cs @@ -18,14 +18,10 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Threading; using System.Windows.Documents; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Education.Commands; +using SonarLint.VisualStudio.Core.Suppressions; using SonarLint.VisualStudio.Education.Rule; using SonarLint.VisualStudio.Education.XamlGenerator; using SonarLint.VisualStudio.TestInfrastructure; @@ -53,11 +49,11 @@ public void ShowRuleHelp_KnownRule_DocumentIsDisplayedInToolWindow() var ruleId = new SonarCompositeRuleId("repoKey", "ruleKey"); var ruleInfo = Mock.Of(); - ruleMetaDataProvider.Setup(x => x.GetRuleInfoAsync(It.IsAny())).ReturnsAsync(ruleInfo); + 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 */ null)).Returns(flowDocument); + ruleHelpXamlBuilder.Setup(x => x.Create(ruleInfo, /* todo by SLVS-1630 */ null)).Returns(flowDocument); var ruleDescriptionToolWindow = new Mock(); @@ -74,10 +70,10 @@ public void ShowRuleHelp_KnownRule_DocumentIsDisplayedInToolWindow() toolWindowService.Invocations.Should().HaveCount(0); // Act - testSubject.ShowRuleHelp(ruleId, null); + testSubject.ShowRuleHelp(ruleId, null, null); - ruleMetaDataProvider.Verify(x => x.GetRuleInfoAsync(ruleId), Times.Once); - ruleHelpXamlBuilder.Verify(x => x.Create(ruleInfo, /* todo */ null), Times.Once); + 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); @@ -95,9 +91,9 @@ public void ShowRuleHelp_FailedToDisplayRule_RuleIsShownInBrowser() var ruleId = new SonarCompositeRuleId("repoKey", "ruleKey"); var ruleInfo = Mock.Of(); - ruleMetadataProvider.Setup(x => x.GetRuleInfoAsync(It.IsAny())).ReturnsAsync(ruleInfo); + ruleMetadataProvider.Setup(x => x.GetRuleInfoAsync(It.IsAny(), It.IsAny())).ReturnsAsync(ruleInfo); - ruleHelpXamlBuilder.Setup(x => x.Create(ruleInfo, /* todo */ null)).Throws(new Exception("some layout error")); + ruleHelpXamlBuilder.Setup(x => x.Create(ruleInfo, /* todo by SLVS-1630 */ null)).Throws(new Exception("some layout error")); var testSubject = CreateEducation( toolWindowService.Object, @@ -107,9 +103,9 @@ public void ShowRuleHelp_FailedToDisplayRule_RuleIsShownInBrowser() toolWindowService.Reset(); // Called in the constructor, so need to reset to clear the list of invocations - testSubject.ShowRuleHelp(ruleId, /* todo */ null); + testSubject.ShowRuleHelp(ruleId, null, /* todo by SLVS-1630 */null); - ruleMetadataProvider.Verify(x => x.GetRuleInfoAsync(ruleId), Times.Once); + 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 @@ -126,7 +122,7 @@ public void ShowRuleHelp_UnknownRule_RuleIsShownInBrowser() var showRuleInBrowser = new Mock(); var unknownRule = new SonarCompositeRuleId("known", "xxx"); - ruleMetadataProvider.Setup(x => x.GetRuleInfoAsync(unknownRule)).ReturnsAsync((IRuleInfo)null); + ruleMetadataProvider.Setup(x => x.GetRuleInfoAsync(unknownRule, It.IsAny())).ReturnsAsync((IRuleInfo)null); var testSubject = CreateEducation( toolWindowService.Object, @@ -136,9 +132,9 @@ public void ShowRuleHelp_UnknownRule_RuleIsShownInBrowser() toolWindowService.Reset(); // Called in the constructor, so need to reset to clear the list of invocations - testSubject.ShowRuleHelp(unknownRule, /* todo */ null); + testSubject.ShowRuleHelp(unknownRule, null, /* todo by SLVS-1630 */ null); - ruleMetadataProvider.Verify(x => x.GetRuleInfoAsync(unknownRule), Times.Once); + 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 @@ -146,6 +142,27 @@ public void ShowRuleHelp_UnknownRule_RuleIsShownInBrowser() 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, diff --git a/src/Education.UnitTests/ErrorList/SonarErrorListEventProcessorTests.cs b/src/Education.UnitTests/ErrorList/SonarErrorListEventProcessorTests.cs index f81eeeb0c5..8ce2c980d8 100644 --- a/src/Education.UnitTests/ErrorList/SonarErrorListEventProcessorTests.cs +++ b/src/Education.UnitTests/ErrorList/SonarErrorListEventProcessorTests.cs @@ -18,11 +18,10 @@ * 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.Core.Suppressions; using SonarLint.VisualStudio.Education.SonarLint.VisualStudio.Education.ErrorList; using SonarLint.VisualStudio.Infrastructure.VS; using SonarLint.VisualStudio.TestInfrastructure; @@ -68,10 +67,30 @@ public void PreprocessNavigateToHelp_IsASonarRule_EventIsHandledAndEducationServ errorListHelper.Verify(x => x.TryGetRuleId(handle, out ruleId)); education.Invocations.Should().HaveCount(1); - education.Verify(x => x.ShowRuleHelp(ruleId, /* todo */ null)); + education.Verify(x => x.ShowRuleHelp(ruleId, null, /* todo by SLVS-1630 */ null)); eventArgs.Handled.Should().BeTrue(); } + [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) { var mock = new Mock(); diff --git a/src/Education.UnitTests/Rule/RuleInfoConverterTests.cs b/src/Education.UnitTests/Rule/RuleInfoConverterTests.cs new file mode 100644 index 0000000000..a857fe0145 --- /dev/null +++ b/src/Education.UnitTests/Rule/RuleInfoConverterTests.cs @@ -0,0 +1,430 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Education.Rule; +using SonarLint.VisualStudio.SLCore.Common.Models; +using SonarLint.VisualStudio.SLCore.Protocol; +using SonarLint.VisualStudio.SLCore.Service.Issue.Models; +using SonarLint.VisualStudio.SLCore.Service.Rules.Models; +using SonarLint.VisualStudio.TestInfrastructure; +using CleanCodeAttribute = SonarLint.VisualStudio.SLCore.Common.Models.CleanCodeAttribute; +using IssueSeverity = SonarLint.VisualStudio.SLCore.Common.Models.IssueSeverity; +using Language = SonarLint.VisualStudio.SLCore.Common.Models.Language; +using SoftwareQuality = SonarLint.VisualStudio.SLCore.Common.Models.SoftwareQuality; +using RuleCleanCodeAttribute = SonarLint.VisualStudio.Core.Analysis.CleanCodeAttribute; +using RuleSoftwareQuality = SonarLint.VisualStudio.Core.Analysis.SoftwareQuality; +using RuleSoftwareQualitySeverity = SonarLint.VisualStudio.Core.Analysis.SoftwareQualitySeverity; + +namespace SonarLint.VisualStudio.Education.UnitTests.Rule; + +[TestClass] +public class RuleInfoConverterTests +{ + private RuleInfoConverter testSubject; + + [TestInitialize] + public void TestInitialize() => testSubject = new RuleInfoConverter(); + + [TestMethod] + public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported(); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [DataTestMethod] + [DataRow(IssueSeverity.INFO, RuleIssueSeverity.Info)] + [DataRow(IssueSeverity.MAJOR, RuleIssueSeverity.Major)] + [DataRow(IssueSeverity.BLOCKER, RuleIssueSeverity.Blocker)] + [DataRow(IssueSeverity.CRITICAL, RuleIssueSeverity.Critical)] + [DataRow(IssueSeverity.MINOR, RuleIssueSeverity.Minor)] + public void Convert_RuleDetails_CorrectlyConvertsSeverity(IssueSeverity slCore, RuleIssueSeverity expected) + { + var ruleDetails = new EffectiveRuleDetailsDto( + default, + default, + default, + new StandardModeDetails(slCore, default), + default, + default, + default); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.Severity.Should().Be(expected); + } + + [DataTestMethod] + [DataRow(RuleType.CODE_SMELL, RuleIssueType.CodeSmell)] + [DataRow(RuleType.VULNERABILITY, RuleIssueType.Vulnerability)] + [DataRow(RuleType.BUG, RuleIssueType.Bug)] + [DataRow(RuleType.SECURITY_HOTSPOT, RuleIssueType.Hotspot)] + public void Convert_RuleDetails_CorrectlyConvertsType(RuleType slCore, RuleIssueType expected) + { + var ruleDetails = new EffectiveRuleDetailsDto( + default, + default, + default, + new StandardModeDetails(default, slCore), + default, + default, + default); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.IssueType.Should().Be(expected); + } + + [DataTestMethod] + [DataRow(CleanCodeAttribute.CONVENTIONAL, RuleCleanCodeAttribute.Conventional)] + [DataRow(CleanCodeAttribute.FORMATTED, RuleCleanCodeAttribute.Formatted)] + [DataRow(CleanCodeAttribute.IDENTIFIABLE, RuleCleanCodeAttribute.Identifiable)] + [DataRow(CleanCodeAttribute.CLEAR, RuleCleanCodeAttribute.Clear)] + [DataRow(CleanCodeAttribute.COMPLETE, RuleCleanCodeAttribute.Complete)] + [DataRow(CleanCodeAttribute.EFFICIENT, RuleCleanCodeAttribute.Efficient)] + [DataRow(CleanCodeAttribute.LOGICAL, RuleCleanCodeAttribute.Logical)] + [DataRow(CleanCodeAttribute.DISTINCT, RuleCleanCodeAttribute.Distinct)] + [DataRow(CleanCodeAttribute.FOCUSED, RuleCleanCodeAttribute.Focused)] + [DataRow(CleanCodeAttribute.MODULAR, RuleCleanCodeAttribute.Modular)] + [DataRow(CleanCodeAttribute.TESTED, RuleCleanCodeAttribute.Tested)] + [DataRow(CleanCodeAttribute.LAWFUL, RuleCleanCodeAttribute.Lawful)] + [DataRow(CleanCodeAttribute.RESPECTFUL, RuleCleanCodeAttribute.Respectful)] + [DataRow(CleanCodeAttribute.TRUSTWORTHY, RuleCleanCodeAttribute.Trustworthy)] + public void Convert_RuleDetails_CorrectlyConvertsCleanCodeAttribute(CleanCodeAttribute slCore, RuleCleanCodeAttribute expected) + { + var ruleDetails = new EffectiveRuleDetailsDto( + default, + default, + default, + new MQRModeDetails(slCore, default), + default, + default, + default); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.CleanCodeAttribute.Should().Be(expected); + } + + [TestMethod] + public void Convert_RuleDetails_CorrectlyConvertsImpacts() + { + var ruleDetails = new EffectiveRuleDetailsDto( + default, + default, + default, + new MQRModeDetails(default, [ + new ImpactDto(SoftwareQuality.SECURITY, ImpactSeverity.HIGH), + new ImpactDto(SoftwareQuality.RELIABILITY, ImpactSeverity.LOW), + new ImpactDto(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.MEDIUM) + ]), + default, + default, + default); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.DefaultImpacts.Should().BeEquivalentTo(new Dictionary + { + { RuleSoftwareQuality.Security, RuleSoftwareQualitySeverity.High }, + { RuleSoftwareQuality.Reliability, RuleSoftwareQualitySeverity.Low }, + { RuleSoftwareQuality.Maintainability, RuleSoftwareQualitySeverity.Medium } + }); + } + + [TestMethod] + public void Convert_RuleDetails_Standard_SimpleRuleDescription() + { + const string rulekey = "rule:key1"; + var ruleDetails = new EffectiveRuleDetailsDto( + rulekey, + "name", + Language.JS, + new StandardModeDetails(IssueSeverity.CRITICAL, RuleType.VULNERABILITY), + VulnerabilityProbability.MEDIUM, + Either.CreateLeft( + new RuleMonolithicDescriptionDto("content")), + new List()); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.Should().BeEquivalentTo(new RuleInfo(rulekey, + "content", + "name", + RuleIssueSeverity.Critical, + RuleIssueType.Vulnerability, + null, + null, + null)); + } + + [TestMethod] + public void Convert_RuleDetails_MQR_SimpleRuleDescription() + { + const string rulekey = "rule:key1"; + var ruleDetails = new EffectiveRuleDetailsDto( + rulekey, + "name", + Language.JS, + new MQRModeDetails(CleanCodeAttribute.MODULAR, [ + new ImpactDto(SoftwareQuality.SECURITY, ImpactSeverity.HIGH), + new ImpactDto(SoftwareQuality.RELIABILITY, ImpactSeverity.LOW) + ]), + VulnerabilityProbability.MEDIUM, + Either.CreateLeft( + new RuleMonolithicDescriptionDto("content")), + new List()); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.Should().BeEquivalentTo(new RuleInfo(rulekey, + "content", + "name", + null, + null, + null, + RuleCleanCodeAttribute.Modular, + new Dictionary + { + { RuleSoftwareQuality.Security, RuleSoftwareQualitySeverity.High }, { RuleSoftwareQuality.Reliability, RuleSoftwareQualitySeverity.Low } + })); + } + + [TestMethod] + public void Convert_RuleDetails_Standard_RichRuleDescription() + { + const string rulekey = "rule:key1"; + var ruleSplitDescriptionDto = new RuleSplitDescriptionDto("intro", new List()); + var ruleDetails = new EffectiveRuleDetailsDto( + rulekey, + "name", + Language.CPP, + new StandardModeDetails(IssueSeverity.MINOR, RuleType.BUG), + null, + Either.CreateRight(ruleSplitDescriptionDto), + new List { new("ignored", default, default, default) }); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.Should().BeEquivalentTo(new RuleInfo(rulekey, + null, + "name", + RuleIssueSeverity.Minor, + RuleIssueType.Bug, + ruleSplitDescriptionDto, + null, + null)); + } + + [TestMethod] + public void Convert_RuleDetails_MQR_RichRuleDescription() + { + const string rulekey = "rule:key1"; + var ruleSplitDescriptionDto = new RuleSplitDescriptionDto("intro", new List()); + var ruleDetails = new EffectiveRuleDetailsDto( + rulekey, + "name", + Language.CPP, + new MQRModeDetails(CleanCodeAttribute.RESPECTFUL, [ + new ImpactDto(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.MEDIUM) + ]), + null, + Either.CreateRight(ruleSplitDescriptionDto), + new List { new("ignored", default, default, default) }); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.Should().BeEquivalentTo(new RuleInfo(rulekey, + null, + "name", + null, + null, + ruleSplitDescriptionDto, + RuleCleanCodeAttribute.Respectful, + new Dictionary { { RuleSoftwareQuality.Maintainability, RuleSoftwareQualitySeverity.Medium } })); + } + + [DataTestMethod] + [DataRow(IssueSeverity.INFO, RuleIssueSeverity.Info)] + [DataRow(IssueSeverity.MAJOR, RuleIssueSeverity.Major)] + [DataRow(IssueSeverity.BLOCKER, RuleIssueSeverity.Blocker)] + [DataRow(IssueSeverity.CRITICAL, RuleIssueSeverity.Critical)] + [DataRow(IssueSeverity.MINOR, RuleIssueSeverity.Minor)] + public void Convert_IssueDetails_CorrectlyConvertsSeverity(IssueSeverity slCore, RuleIssueSeverity expected) + { + Guid.NewGuid(); + var ruleDetails = CreateEffectiveIssueDetailsDto(new StandardModeDetails(slCore, default)); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.Severity.Should().Be(expected); + } + + [DataTestMethod] + [DataRow(RuleType.CODE_SMELL, RuleIssueType.CodeSmell)] + [DataRow(RuleType.VULNERABILITY, RuleIssueType.Vulnerability)] + [DataRow(RuleType.BUG, RuleIssueType.Bug)] + [DataRow(RuleType.SECURITY_HOTSPOT, RuleIssueType.Hotspot)] + public void Convert_IssueDetails_CorrectlyConvertsType(RuleType slCore, RuleIssueType expected) + { + Guid.NewGuid(); + var ruleDetails = CreateEffectiveIssueDetailsDto(new StandardModeDetails(default, slCore)); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.IssueType.Should().Be(expected); + } + + [DataTestMethod] + [DataRow(CleanCodeAttribute.CONVENTIONAL, RuleCleanCodeAttribute.Conventional)] + [DataRow(CleanCodeAttribute.FORMATTED, RuleCleanCodeAttribute.Formatted)] + [DataRow(CleanCodeAttribute.IDENTIFIABLE, RuleCleanCodeAttribute.Identifiable)] + [DataRow(CleanCodeAttribute.CLEAR, RuleCleanCodeAttribute.Clear)] + [DataRow(CleanCodeAttribute.COMPLETE, RuleCleanCodeAttribute.Complete)] + [DataRow(CleanCodeAttribute.EFFICIENT, RuleCleanCodeAttribute.Efficient)] + [DataRow(CleanCodeAttribute.LOGICAL, RuleCleanCodeAttribute.Logical)] + [DataRow(CleanCodeAttribute.DISTINCT, RuleCleanCodeAttribute.Distinct)] + [DataRow(CleanCodeAttribute.FOCUSED, RuleCleanCodeAttribute.Focused)] + [DataRow(CleanCodeAttribute.MODULAR, RuleCleanCodeAttribute.Modular)] + [DataRow(CleanCodeAttribute.TESTED, RuleCleanCodeAttribute.Tested)] + [DataRow(CleanCodeAttribute.LAWFUL, RuleCleanCodeAttribute.Lawful)] + [DataRow(CleanCodeAttribute.RESPECTFUL, RuleCleanCodeAttribute.Respectful)] + [DataRow(CleanCodeAttribute.TRUSTWORTHY, RuleCleanCodeAttribute.Trustworthy)] + public void Convert_IssueDetails_CorrectlyConvertsCleanCodeAttribute(CleanCodeAttribute slCore, RuleCleanCodeAttribute expected) + { + Guid.NewGuid(); + var ruleDetails = CreateEffectiveIssueDetailsDto(new MQRModeDetails(slCore, default)); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.CleanCodeAttribute.Should().Be(expected); + } + + [TestMethod] + public void Convert_IssueDetails_CorrectlyConvertsImpacts() + { + Guid.NewGuid(); + var ruleDetails = CreateEffectiveIssueDetailsDto(new MQRModeDetails(default, [ + new ImpactDto(SoftwareQuality.SECURITY, ImpactSeverity.HIGH), + new ImpactDto(SoftwareQuality.RELIABILITY, ImpactSeverity.LOW), + new ImpactDto(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.MEDIUM) + ])); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.DefaultImpacts.Should().BeEquivalentTo(new Dictionary + { + { RuleSoftwareQuality.Security, RuleSoftwareQualitySeverity.High }, + { RuleSoftwareQuality.Reliability, RuleSoftwareQualitySeverity.Low }, + { RuleSoftwareQuality.Maintainability, RuleSoftwareQualitySeverity.Medium } + }); + } + + [TestMethod] + public void Convert_IssueDetails_Standard_SimpleRuleDescription() + { + Guid.NewGuid(); + var ruleDetails = CreateEffectiveIssueDetailsDto(new StandardModeDetails(IssueSeverity.CRITICAL, RuleType.VULNERABILITY), + Either.CreateLeft( + new RuleMonolithicDescriptionDto("content"))); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.Should().BeEquivalentTo(new RuleInfo(null, + "content", + null, + RuleIssueSeverity.Critical, + RuleIssueType.Vulnerability, + null, + null, + null)); + } + + [TestMethod] + public void Convert_IssueDetails_MQR_SimpleRuleDescription() + { + Guid.NewGuid(); + var ruleDetails = CreateEffectiveIssueDetailsDto(new MQRModeDetails(CleanCodeAttribute.MODULAR, default), Either.CreateLeft( + new RuleMonolithicDescriptionDto("content"))); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.Should().BeEquivalentTo(new RuleInfo(null, + "content", + null, + null, + null, + null, + RuleCleanCodeAttribute.Modular, + null)); + } + + [TestMethod] + public void Convert_IssueDetails_Standard_RichRuleDescription() + { + Guid.NewGuid(); + var ruleSplitDescriptionDto = new RuleSplitDescriptionDto("intro", new List()); + var ruleDetails = CreateEffectiveIssueDetailsDto(new StandardModeDetails(IssueSeverity.MINOR, RuleType.BUG), + Either.CreateRight(ruleSplitDescriptionDto)); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.Should().BeEquivalentTo(new RuleInfo(null, + null, + null, + RuleIssueSeverity.Minor, + RuleIssueType.Bug, + ruleSplitDescriptionDto, + null, + null)); + } + + [TestMethod] + public void Convert_IssueDetails_MQR_RichRuleDescription() + { + Guid.NewGuid(); + var ruleSplitDescriptionDto = new RuleSplitDescriptionDto("intro", new List()); + var ruleDetails = CreateEffectiveIssueDetailsDto(new MQRModeDetails(CleanCodeAttribute.RESPECTFUL, default), + Either.CreateRight(ruleSplitDescriptionDto)); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.Should().BeEquivalentTo(new RuleInfo(null, + null, + null, + null, + null, + ruleSplitDescriptionDto, + RuleCleanCodeAttribute.Respectful, + null)); + } + + private static EffectiveIssueDetailsDto CreateEffectiveIssueDetailsDto( + Either severityDetails, + Either description = default) => + new( + default, + default, + default, + default, + description, + default, + severityDetails, + default); +} diff --git a/src/Education.UnitTests/Rule/SLCoreRuleMetaDataProviderTests.cs b/src/Education.UnitTests/Rule/SLCoreRuleMetaDataProviderTests.cs index 1309d6d22f..676b139045 100644 --- a/src/Education.UnitTests/Rule/SLCoreRuleMetaDataProviderTests.cs +++ b/src/Education.UnitTests/Rule/SLCoreRuleMetaDataProviderTests.cs @@ -21,343 +21,208 @@ using Moq; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Education.Rule; -using SonarLint.VisualStudio.SLCore.Common.Models; using SonarLint.VisualStudio.SLCore.Core; using SonarLint.VisualStudio.SLCore.Protocol; using SonarLint.VisualStudio.SLCore.Service.Rules; using SonarLint.VisualStudio.SLCore.Service.Rules.Models; using SonarLint.VisualStudio.SLCore.State; using SonarLint.VisualStudio.TestInfrastructure; -using CleanCodeAttribute = SonarLint.VisualStudio.SLCore.Common.Models.CleanCodeAttribute; -using IssueSeverity = SonarLint.VisualStudio.SLCore.Common.Models.IssueSeverity; -using Language = SonarLint.VisualStudio.SLCore.Common.Models.Language; -using SoftwareQuality = SonarLint.VisualStudio.SLCore.Common.Models.SoftwareQuality; -using RuleCleanCodeAttribute = SonarLint.VisualStudio.Core.Analysis.CleanCodeAttribute; -using RuleSoftwareQuality = SonarLint.VisualStudio.Core.Analysis.SoftwareQuality; -using RuleSoftwareQualitySeverity = SonarLint.VisualStudio.Core.Analysis.SoftwareQualitySeverity; +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"); + [TestMethod] public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported( MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); [TestMethod] public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); - [DataTestMethod] - [DataRow(IssueSeverity.INFO, RuleIssueSeverity.Info)] - [DataRow(IssueSeverity.MAJOR, RuleIssueSeverity.Major)] - [DataRow(IssueSeverity.BLOCKER, RuleIssueSeverity.Blocker)] - [DataRow(IssueSeverity.CRITICAL, RuleIssueSeverity.Critical)] - [DataRow(IssueSeverity.MINOR, RuleIssueSeverity.Minor)] - public async Task GetRuleInfoAsync_CorrectlyConvertsSeverity(IssueSeverity slCore, RuleIssueSeverity expected) + [TestMethod] + public async Task GetRuleInfoAsync_NoActiveScope_ReturnsNull() { - const string rulekey = "rule:key1"; - const string configScopeId = "configscope"; - var testSubject = - CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out _); - SetUpServiceProvider(serviceProviderMock, out var rulesServiceMock); - SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope(configScopeId)); - SetupRulesService(rulesServiceMock, rulekey, configScopeId, new EffectiveRuleDetailsDto( - default, - default, - default, - new StandardModeDetails(slCore, default), - default, - default, - default)); + CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out var logger); + SetUpServiceProvider(serviceProviderMock, out _); + SetUpConfigScopeTracker(configScopeTrackerMock, null); - var ruleInfo = await testSubject.GetRuleInfoAsync(new SonarCompositeRuleId("rule", "key1")); + var ruleInfo = await testSubject.GetRuleInfoAsync(CompositeRuleId); - ruleInfo.Severity.Should().Be(expected); + ruleInfo.Should().BeNull(); + logger.AssertNoOutputMessages(); } - [DataTestMethod] - [DataRow(RuleType.CODE_SMELL, RuleIssueType.CodeSmell)] - [DataRow(RuleType.VULNERABILITY, RuleIssueType.Vulnerability)] - [DataRow(RuleType.BUG, RuleIssueType.Bug)] - [DataRow(RuleType.SECURITY_HOTSPOT, RuleIssueType.Hotspot)] - public async Task GetRuleInfoAsync_CorrectlyConvertsType(RuleType slCore, RuleIssueType expected) + [TestMethod] + public async Task GetRuleInfoAsync_ServiceUnavailable_ReturnsNull() { - const string rulekey = "rule:key1"; - const string configScopeId = "configscope"; - - var testSubject = - CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out _); - SetUpServiceProvider(serviceProviderMock, out var rulesServiceMock); - SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope(configScopeId)); - SetupRulesService(rulesServiceMock, rulekey, configScopeId, new EffectiveRuleDetailsDto( - default, - default, - default, - new StandardModeDetails(default, slCore), - default, - default, - default)); + var testSubject = CreateTestSubject(out _, out var configScopeTrackerMock, out var logger); + SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope("id")); - var ruleInfo = await testSubject.GetRuleInfoAsync(new SonarCompositeRuleId("rule", "key1")); + var ruleInfo = await testSubject.GetRuleInfoAsync(CompositeRuleId); - ruleInfo.IssueType.Should().Be(expected); + ruleInfo.Should().BeNull(); + logger.AssertNoOutputMessages(); } - [DataTestMethod] - [DataRow(CleanCodeAttribute.CONVENTIONAL, RuleCleanCodeAttribute.Conventional)] - [DataRow(CleanCodeAttribute.FORMATTED, RuleCleanCodeAttribute.Formatted)] - [DataRow(CleanCodeAttribute.IDENTIFIABLE, RuleCleanCodeAttribute.Identifiable)] - [DataRow(CleanCodeAttribute.CLEAR, RuleCleanCodeAttribute.Clear)] - [DataRow(CleanCodeAttribute.COMPLETE, RuleCleanCodeAttribute.Complete)] - [DataRow(CleanCodeAttribute.EFFICIENT, RuleCleanCodeAttribute.Efficient)] - [DataRow(CleanCodeAttribute.LOGICAL, RuleCleanCodeAttribute.Logical)] - [DataRow(CleanCodeAttribute.DISTINCT, RuleCleanCodeAttribute.Distinct)] - [DataRow(CleanCodeAttribute.FOCUSED, RuleCleanCodeAttribute.Focused)] - [DataRow(CleanCodeAttribute.MODULAR, RuleCleanCodeAttribute.Modular)] - [DataRow(CleanCodeAttribute.TESTED, RuleCleanCodeAttribute.Tested)] - [DataRow(CleanCodeAttribute.LAWFUL, RuleCleanCodeAttribute.Lawful)] - [DataRow(CleanCodeAttribute.RESPECTFUL, RuleCleanCodeAttribute.Respectful)] - [DataRow(CleanCodeAttribute.TRUSTWORTHY, RuleCleanCodeAttribute.Trustworthy)] - public async Task GetRuleInfoAsync_CorrectlyConvertsCleanCodeAttribute(CleanCodeAttribute slCore, RuleCleanCodeAttribute expected) + [TestMethod] + public void GetRuleInfoAsync_ServiceThrows_ReturnsNullAndLogs() { - const string rulekey = "rule:key1"; - const string configScopeId = "configscope"; - var testSubject = - CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out _); + CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out var logger); + SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope("id")); SetUpServiceProvider(serviceProviderMock, out var rulesServiceMock); - SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope(configScopeId)); - SetupRulesService(rulesServiceMock, rulekey, configScopeId, new EffectiveRuleDetailsDto( - default, - default, - default, - new MQRModeDetails(slCore, default), - default, - default, - default)); + rulesServiceMock + .Setup(x => x.GetEffectiveRuleDetailsAsync(It.IsAny())) + .ThrowsAsync(new Exception("my message")); - var ruleInfo = await testSubject.GetRuleInfoAsync(new SonarCompositeRuleId("rule", "key1")); + var act = () => testSubject.GetRuleInfoAsync(CompositeRuleId); - ruleInfo.CleanCodeAttribute.Should().Be(expected); + act.Should().NotThrow(); + logger.AssertPartialOutputStringExists("my message"); } [TestMethod] - public async Task GetRuleInfoAsync_CorrectlyConvertsImpacts() + public async Task GetRuleInfoAsync_ForIssue_NoActiveScope_ReturnsNull() { - const string rulekey = "rule:key1"; - const string configScopeId = "configscope"; - var testSubject = - CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out _); - SetUpServiceProvider(serviceProviderMock, out var rulesServiceMock); - SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope(configScopeId)); - SetupRulesService(rulesServiceMock, rulekey, configScopeId, new EffectiveRuleDetailsDto( - default, - default, - default, - new MQRModeDetails(default, [ - new ImpactDto(SoftwareQuality.SECURITY, ImpactSeverity.HIGH), - new ImpactDto(SoftwareQuality.RELIABILITY, ImpactSeverity.LOW), - new ImpactDto(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.MEDIUM) - ]), - default, - default, - default)); + CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out var logger); + SetUpIssueServiceProvider(serviceProviderMock, out _); + SetUpConfigScopeTracker(configScopeTrackerMock, null); + var issueId = Guid.NewGuid(); - var ruleInfo = await testSubject.GetRuleInfoAsync(new SonarCompositeRuleId("rule", "key1")); + var ruleInfo = await testSubject.GetRuleInfoAsync(default,issueId); - ruleInfo.DefaultImpacts.Should().BeEquivalentTo(new Dictionary - { - { RuleSoftwareQuality.Security, RuleSoftwareQualitySeverity.High }, - { RuleSoftwareQuality.Reliability, RuleSoftwareQualitySeverity.Low }, - { RuleSoftwareQuality.Maintainability, RuleSoftwareQualitySeverity.Medium } - }); + ruleInfo.Should().BeNull(); + logger.AssertNoOutputMessages(); } [TestMethod] - public async Task GetRuleInfoAsync_Standard_SimpleRuleDescription() + public async Task GetRuleInfoAsync_ForIssue_ServiceUnavailable_ReturnsNull() { - const string rulekey = "rule:key1"; - const string configScopeId = "configscope"; + var testSubject = CreateTestSubject(out _, out var configScopeTrackerMock, out var logger); + SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope("id")); - var testSubject = - CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out var logger); - SetUpServiceProvider(serviceProviderMock, out var rulesServiceMock); - SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope(configScopeId)); - SetupRulesService(rulesServiceMock, rulekey, configScopeId, new EffectiveRuleDetailsDto( - rulekey, - "name", - Language.JS, - new StandardModeDetails(IssueSeverity.CRITICAL, RuleType.VULNERABILITY), - VulnerabilityProbability.MEDIUM, - Either.CreateLeft( - new RuleMonolithicDescriptionDto("content")), - new List())); - - var ruleInfo = await testSubject.GetRuleInfoAsync(new SonarCompositeRuleId("rule", "key1")); - - ruleInfo.Should().BeEquivalentTo(new RuleInfo(rulekey, - "content", - "name", - RuleIssueSeverity.Critical, - RuleIssueType.Vulnerability, - null, - null, - null)); + var ruleInfo = await testSubject.GetRuleInfoAsync(default,Guid.NewGuid()); + + ruleInfo.Should().BeNull(); + logger.AssertNoOutputMessages(); } [TestMethod] - public async Task GetRuleInfoAsync_MQR_SimpleRuleDescription() + public void GetRuleInfoAsync_ForIssue_ServiceThrows_ReturnsNullAndLogs() { - const string rulekey = "rule:key1"; - const string configScopeId = "configscope"; - var testSubject = CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out var logger); - SetUpServiceProvider(serviceProviderMock, out var rulesServiceMock); - SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope(configScopeId)); - SetupRulesService(rulesServiceMock, rulekey, configScopeId, new EffectiveRuleDetailsDto( - rulekey, - "name", - Language.JS, - new MQRModeDetails(CleanCodeAttribute.MODULAR, [ - new ImpactDto(SoftwareQuality.SECURITY, ImpactSeverity.HIGH), - new ImpactDto(SoftwareQuality.RELIABILITY, ImpactSeverity.LOW) - ]), - VulnerabilityProbability.MEDIUM, - Either.CreateLeft( - new RuleMonolithicDescriptionDto("content")), - new List())); - - var ruleInfo = await testSubject.GetRuleInfoAsync(new SonarCompositeRuleId("rule", "key1")); - - ruleInfo.Should().BeEquivalentTo(new RuleInfo(rulekey, - "content", - "name", - null, - null, - null, - RuleCleanCodeAttribute.Modular, - new Dictionary - { - { RuleSoftwareQuality.Security, RuleSoftwareQualitySeverity.High }, { RuleSoftwareQuality.Reliability, RuleSoftwareQualitySeverity.Low } - })); + SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope("id")); + SetUpIssueServiceProvider(serviceProviderMock, out var issueServiceMock); + issueServiceMock + .Setup(x => x.GetEffectiveIssueDetailsAsync(It.IsAny())) + .ThrowsAsync(new Exception("my message")); + + var act = () => testSubject.GetRuleInfoAsync(default,Guid.NewGuid()); + + act.Should().NotThrow(); + logger.AssertPartialOutputStringExists("my message"); } [TestMethod] - public async Task GetRuleInfoAsync_Standard_RichRuleDescription() + public async Task GetRuleInfoAsync_FilterableIssueNull_CallsGetEffectiveRuleDetailsAsync() { - const string rulekey = "rule:key1"; - const string configScopeId = "configscope"; - var testSubject = - CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out var logger); + CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out _); + SetUpIssueServiceProvider(serviceProviderMock, out var issueServiceMock); SetUpServiceProvider(serviceProviderMock, out var rulesServiceMock); - SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope(configScopeId)); - var ruleSplitDescriptionDto = new RuleSplitDescriptionDto("intro", new List()); - SetupRulesService(rulesServiceMock, rulekey, configScopeId, new EffectiveRuleDetailsDto( - rulekey, - "name", - Language.CPP, - new StandardModeDetails(IssueSeverity.MINOR, RuleType.BUG), - null, - Either.CreateRight(ruleSplitDescriptionDto), - new List { new("ignored", default, default, default) })); - - var ruleInfo = await testSubject.GetRuleInfoAsync(new SonarCompositeRuleId("rule", "key1")); - - ruleInfo.Should().BeEquivalentTo(new RuleInfo(rulekey, - null, - "name", - RuleIssueSeverity.Minor, - RuleIssueType.Bug, - ruleSplitDescriptionDto, - null, - null)); - logger.AssertNoOutputMessages(); + SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope("configscope")); + + 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); } [TestMethod] - public async Task GetRuleInfoAsync_MQR_RichRuleDescription() + public async Task GetRuleInfoAsync_FilterableIssueIdNull_CallsGetEffectiveRuleDetailsAsync() { - const string rulekey = "rule:key1"; - const string configScopeId = "configscope"; - var testSubject = - CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out var logger); + CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out _); + SetUpIssueServiceProvider(serviceProviderMock, out var issueServiceMock); SetUpServiceProvider(serviceProviderMock, out var rulesServiceMock); - SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope(configScopeId)); - var ruleSplitDescriptionDto = new RuleSplitDescriptionDto("intro", new List()); - SetupRulesService(rulesServiceMock, rulekey, configScopeId, new EffectiveRuleDetailsDto( - rulekey, - "name", - Language.CPP, - new MQRModeDetails(CleanCodeAttribute.RESPECTFUL, [ - new ImpactDto(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.MEDIUM) - ]), - null, - Either.CreateRight(ruleSplitDescriptionDto), - new List { new("ignored", default, default, default) })); - - var ruleInfo = await testSubject.GetRuleInfoAsync(new SonarCompositeRuleId("rule", "key1")); - - ruleInfo.Should().BeEquivalentTo(new RuleInfo(rulekey, - null, - "name", - null, - null, - ruleSplitDescriptionDto, - RuleCleanCodeAttribute.Respectful, - new Dictionary { { RuleSoftwareQuality.Maintainability, RuleSoftwareQualitySeverity.Medium } })); - logger.AssertNoOutputMessages(); + SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope("configscope")); + Guid? issueId = null; + + 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); } [TestMethod] - public async Task GetRuleInfoAsync_NoActiveScope_ReturnsNull() + public async Task GetRuleInfoAsync_FilterableIssueIdNotNull_CallsGetEffectiveIssueDetailsAsync() { - var testSubject = - CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out var logger); - SetUpServiceProvider(serviceProviderMock, out _); - SetUpConfigScopeTracker(configScopeTrackerMock, null); + 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))); - var ruleInfo = await testSubject.GetRuleInfoAsync(new SonarCompositeRuleId("rule", "key1")); + await testSubject.GetRuleInfoAsync(CompositeRuleId, issueId); - ruleInfo.Should().BeNull(); - logger.AssertNoOutputMessages(); + rulesServiceMock.Verify(x => x.GetEffectiveRuleDetailsAsync(It.IsAny()), Times.Never); + issueServiceMock.Verify(x => x.GetEffectiveIssueDetailsAsync(It.Is(p => p.issueId == issueId)), Times.Once); } [TestMethod] - public async Task GetRuleInfoAsync_ServiceUnavailable_ReturnsNull() + public async Task GetRuleInfoAsync_GetEffectiveIssueDetailsAsyncThrows_CallsGetEffectiveRuleDetailsAsync() { - var testSubject = CreateTestSubject(out _, out var configScopeTrackerMock, out var logger); - SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope("id")); + 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")); - var ruleInfo = await testSubject.GetRuleInfoAsync(new SonarCompositeRuleId("rule", "key1")); + await testSubject.GetRuleInfoAsync(CompositeRuleId, issueId); - ruleInfo.Should().BeNull(); - logger.AssertNoOutputMessages(); + 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); } [TestMethod] - public void GetRuleInfoAsync_ServiceThrows_ReturnsNullAndLogs() + public async Task GetRuleInfoAsync_BothServicesThrow_ReturnsNull() { var testSubject = - CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out var logger); - SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope("id")); + 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 act = () => testSubject.GetRuleInfoAsync(new SonarCompositeRuleId("rule", "key1")); + var result = await testSubject.GetRuleInfoAsync(CompositeRuleId, issueId); - act.Should().NotThrow(); - logger.AssertPartialOutputStringExists("my message"); + 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); } private static void SetUpConfigScopeTracker( @@ -365,15 +230,14 @@ private static void SetUpConfigScopeTracker( ConfigurationScope scope) => configScopeTrackerMock.SetupGet(x => x.Current).Returns(scope); - private static void SetupRulesService( - Mock rulesServiceMock, - string rulekey, + private static void SetupIssuesService( + Mock issuesServiceMock, + Guid id, string configScopeId, - EffectiveRuleDetailsDto response) => - rulesServiceMock - .Setup(r => r.GetEffectiveRuleDetailsAsync(It.Is(p => - p.ruleKey == rulekey && p.configurationScopeId == configScopeId))) - .ReturnsAsync(new GetEffectiveRuleDetailsResponse(response)); + 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, @@ -384,6 +248,15 @@ private static void SetUpServiceProvider( serviceProviderMock.Setup(x => x.TryGetTransientService(out rulesService)).Returns(true); } + 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 static SLCoreRuleMetaDataProvider CreateTestSubject( out Mock serviceProviderMock, out Mock configScopeTrackerMock, @@ -391,7 +264,22 @@ private static SLCoreRuleMetaDataProvider CreateTestSubject( { 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, logger); + return new SLCoreRuleMetaDataProvider(serviceProviderMock.Object, configScopeTrackerMock.Object, ruleInfoConverter.Object, logger); } + + private static EffectiveIssueDetailsDto CreateEffectiveIssueDetailsDto(Either severityDetails, + Either description = default) => + new( + default, + default, + default, + default, + description, + default, + severityDetails, + default); } diff --git a/src/Education/Controls/RuleHelpUserControl.xaml.cs b/src/Education/Controls/RuleHelpUserControl.xaml.cs index 1eab623ec3..1566f7b904 100644 --- a/src/Education/Controls/RuleHelpUserControl.xaml.cs +++ b/src/Education/Controls/RuleHelpUserControl.xaml.cs @@ -57,7 +57,7 @@ public void HandleRequestNavigate(object sender, RequestNavigateEventArgs e) // in which case it needs to be handed over to the education service. if (SonarRuleIdUriEncoderDecoder.TryDecodeToCompositeRuleId(e.Uri, out SonarCompositeRuleId compositeRuleId)) { - education.ShowRuleHelp(compositeRuleId, null); + education.ShowRuleHelp(compositeRuleId, null, null); return; } diff --git a/src/Education/Education.cs b/src/Education/Education.cs index f1902994e7..c2bb19d406 100644 --- a/src/Education/Education.cs +++ b/src/Education/Education.cs @@ -18,13 +18,10 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using System.ComponentModel.Composition; -using System.Threading; -using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Education.Commands; +using SonarLint.VisualStudio.Core.Suppressions; using SonarLint.VisualStudio.Education.Rule; using SonarLint.VisualStudio.Education.XamlGenerator; using SonarLint.VisualStudio.Infrastructure.VS; @@ -68,16 +65,16 @@ public Education(IToolWindowService toolWindowService, IRuleMetaDataProvider rul this.threadHandling = threadHandling; } - public void ShowRuleHelp(SonarCompositeRuleId ruleId, string issueContext) + public void ShowRuleHelp(SonarCompositeRuleId ruleId, Guid? issueId, string issueContext) { - ShowRuleHelpAsync(ruleId, issueContext).Forget(); + ShowRuleHelpAsync(ruleId, issueId, issueContext).Forget(); } - private async Task ShowRuleHelpAsync(SonarCompositeRuleId ruleId, string issueContext) + private async Task ShowRuleHelpAsync(SonarCompositeRuleId ruleId, Guid? issueId, string issueContext) { await threadHandling.SwitchToBackgroundThread(); - var ruleInfo = await ruleMetadataProvider.GetRuleInfoAsync(ruleId); + var ruleInfo = await ruleMetadataProvider.GetRuleInfoAsync(ruleId, issueId); await threadHandling.RunOnUIThreadAsync(() => { diff --git a/src/Education/ErrorList/SonarErrorListEventProcessor.cs b/src/Education/ErrorList/SonarErrorListEventProcessor.cs index 539a2eb7f7..5c3828a5a2 100644 --- a/src/Education/ErrorList/SonarErrorListEventProcessor.cs +++ b/src/Education/ErrorList/SonarErrorListEventProcessor.cs @@ -56,9 +56,10 @@ public override void PreprocessNavigateToHelp( if (errorListHelper.TryGetRuleId(entry, out var ruleId)) { + errorListHelper.TryGetFilterableIssue(entry, out var filterableIssue); logger.LogVerbose(Resources.ErrorList_Processor_SonarRuleDetected, ruleId); - educationService.ShowRuleHelp(ruleId, /* todo */ null); + educationService.ShowRuleHelp(ruleId, filterableIssue?.IssueId, /* todo by SLVS-1630 */null); // Mark the event as handled to stop the normal VS "show help in browser" behaviour handled = true; diff --git a/src/Education/Rule/IRuleInfoConverter.cs b/src/Education/Rule/IRuleInfoConverter.cs new file mode 100644 index 0000000000..39c4571258 --- /dev/null +++ b/src/Education/Rule/IRuleInfoConverter.cs @@ -0,0 +1,100 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using SonarLint.VisualStudio.Core.Analysis; +using SonarLint.VisualStudio.SLCore.Common.Helpers; +using SonarLint.VisualStudio.SLCore.Common.Models; +using SonarLint.VisualStudio.SLCore.Service.Rules.Models; +using CleanCodeAttribute = SonarLint.VisualStudio.Core.Analysis.CleanCodeAttribute; +using IssueSeverity = SonarLint.VisualStudio.SLCore.Common.Models.IssueSeverity; +using SoftwareQuality = SonarLint.VisualStudio.Core.Analysis.SoftwareQuality; + +namespace SonarLint.VisualStudio.Education.Rule; + +internal interface IRuleInfoConverter +{ + IRuleInfo Convert(IRuleDetails details); +} + +[Export(typeof(IRuleInfoConverter))] +[PartCreationPolicy(CreationPolicy.Shared)] +public class RuleInfoConverter : IRuleInfoConverter +{ + [ImportingConstructor] + public RuleInfoConverter() { } + + public IRuleInfo Convert(IRuleDetails details) => + new RuleInfo(details.key, + HtmlXmlCompatibilityHelper.EnsureHtmlIsXml(details.description?.Left?.htmlContent), + details.name, + Convert(details.severityDetails.Left?.severity), + Convert(details.severityDetails.Left?.type), + details.description?.Right, + Convert(details.severityDetails.Right?.cleanCodeAttribute), + Convert(details.severityDetails.Right?.impacts)); + + private static RuleIssueSeverity? Convert(IssueSeverity? issueSeverity) => + issueSeverity switch + { + IssueSeverity.BLOCKER => RuleIssueSeverity.Blocker, + IssueSeverity.CRITICAL => RuleIssueSeverity.Critical, + IssueSeverity.MAJOR => RuleIssueSeverity.Major, + IssueSeverity.MINOR => RuleIssueSeverity.Minor, + IssueSeverity.INFO => RuleIssueSeverity.Info, + null => null, + _ => throw new ArgumentOutOfRangeException(nameof(issueSeverity), issueSeverity, null) + }; + + private static RuleIssueType? Convert(RuleType? ruleType) => + ruleType switch + { + RuleType.CODE_SMELL => RuleIssueType.CodeSmell, + RuleType.BUG => RuleIssueType.Bug, + RuleType.VULNERABILITY => RuleIssueType.Vulnerability, + RuleType.SECURITY_HOTSPOT => RuleIssueType.Hotspot, + null => null, + _ => throw new ArgumentOutOfRangeException(nameof(ruleType), ruleType, null) + }; + + private static CleanCodeAttribute? Convert(SLCore.Common.Models.CleanCodeAttribute? cleanCodeAttribute) => + cleanCodeAttribute switch + { + SLCore.Common.Models.CleanCodeAttribute.CONVENTIONAL => CleanCodeAttribute.Conventional, + SLCore.Common.Models.CleanCodeAttribute.FORMATTED => CleanCodeAttribute.Formatted, + SLCore.Common.Models.CleanCodeAttribute.IDENTIFIABLE => CleanCodeAttribute.Identifiable, + SLCore.Common.Models.CleanCodeAttribute.CLEAR => CleanCodeAttribute.Clear, + SLCore.Common.Models.CleanCodeAttribute.COMPLETE => CleanCodeAttribute.Complete, + SLCore.Common.Models.CleanCodeAttribute.EFFICIENT => CleanCodeAttribute.Efficient, + SLCore.Common.Models.CleanCodeAttribute.LOGICAL => CleanCodeAttribute.Logical, + SLCore.Common.Models.CleanCodeAttribute.DISTINCT => CleanCodeAttribute.Distinct, + SLCore.Common.Models.CleanCodeAttribute.FOCUSED => CleanCodeAttribute.Focused, + SLCore.Common.Models.CleanCodeAttribute.MODULAR => CleanCodeAttribute.Modular, + SLCore.Common.Models.CleanCodeAttribute.TESTED => CleanCodeAttribute.Tested, + SLCore.Common.Models.CleanCodeAttribute.LAWFUL => CleanCodeAttribute.Lawful, + SLCore.Common.Models.CleanCodeAttribute.RESPECTFUL => CleanCodeAttribute.Respectful, + SLCore.Common.Models.CleanCodeAttribute.TRUSTWORTHY => CleanCodeAttribute.Trustworthy, + null => null, + _ => throw new ArgumentOutOfRangeException(nameof(cleanCodeAttribute), cleanCodeAttribute, null) + }; + + private static Dictionary Convert(List cleanCodeAttribute) => + cleanCodeAttribute?.ToDictionary(x => x.softwareQuality.ToSoftwareQuality(), x => x.impactSeverity.ToSoftwareQualitySeverity()); +} diff --git a/src/Education/Rule/IRuleMetaDataProvider.cs b/src/Education/Rule/IRuleMetaDataProvider.cs index 50f063940a..bf1c9e056e 100644 --- a/src/Education/Rule/IRuleMetaDataProvider.cs +++ b/src/Education/Rule/IRuleMetaDataProvider.cs @@ -18,8 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Threading; -using System.Threading.Tasks; using SonarLint.VisualStudio.Core; namespace SonarLint.VisualStudio.Education.Rule @@ -27,9 +25,10 @@ namespace SonarLint.VisualStudio.Education.Rule public interface IRuleMetaDataProvider { /// - /// Returns rule information for the specified rule ID, or null if a rule description - /// could not be found. + /// If is NOT null, returns rule information for the specified issue ID + /// If is null, returns the rule information for the specified rule ID + /// If no rule information can be found, null is returned. /// - Task GetRuleInfoAsync(SonarCompositeRuleId ruleId); + Task GetRuleInfoAsync(SonarCompositeRuleId ruleId, Guid? issueId = null); } } diff --git a/src/Education/Rule/SLCoreRuleMetaDataProvider.cs b/src/Education/Rule/SLCoreRuleMetaDataProvider.cs index 323982b529..8986f656e9 100644 --- a/src/Education/Rule/SLCoreRuleMetaDataProvider.cs +++ b/src/Education/Rule/SLCoreRuleMetaDataProvider.cs @@ -18,22 +18,12 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Collections.Generic; using System.ComponentModel.Composition; -using System.Linq; -using System.Threading.Tasks; using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Analysis; -using SonarLint.VisualStudio.SLCore.Common.Helpers; -using SonarLint.VisualStudio.SLCore.Common.Models; using SonarLint.VisualStudio.SLCore.Core; +using SonarLint.VisualStudio.SLCore.Service.Issue; using SonarLint.VisualStudio.SLCore.Service.Rules; -using SonarLint.VisualStudio.SLCore.Service.Rules.Models; using SonarLint.VisualStudio.SLCore.State; -using CleanCodeAttribute = SonarLint.VisualStudio.Core.Analysis.CleanCodeAttribute; -using IssueSeverity = SonarLint.VisualStudio.SLCore.Common.Models.IssueSeverity; -using SoftwareQuality = SonarLint.VisualStudio.Core.Analysis.SoftwareQuality; namespace SonarLint.VisualStudio.Education.Rule; @@ -42,28 +32,44 @@ namespace SonarLint.VisualStudio.Education.Rule; internal class SLCoreRuleMetaDataProvider : IRuleMetaDataProvider { private readonly IActiveConfigScopeTracker activeConfigScopeTracker; + private readonly IRuleInfoConverter ruleInfoConverter; private readonly ILogger logger; private readonly ISLCoreServiceProvider slCoreServiceProvider; [ImportingConstructor] public SLCoreRuleMetaDataProvider(ISLCoreServiceProvider slCoreServiceProvider, - IActiveConfigScopeTracker activeConfigScopeTracker, ILogger logger) + IActiveConfigScopeTracker activeConfigScopeTracker, + IRuleInfoConverter ruleInfoConverter, + ILogger logger) { this.slCoreServiceProvider = slCoreServiceProvider; this.activeConfigScopeTracker = activeConfigScopeTracker; + this.ruleInfoConverter = ruleInfoConverter; this.logger = logger; } - public async Task GetRuleInfoAsync(SonarCompositeRuleId ruleId) + /// + public async Task GetRuleInfoAsync(SonarCompositeRuleId ruleId, Guid? issueId = null) { - if (activeConfigScopeTracker.Current is { Id: var configurationScopeId } - && slCoreServiceProvider.TryGetTransientService(out IRulesSLCoreService rulesRpcService)) + if (activeConfigScopeTracker.Current is not { Id: var configurationScopeId }) + { + return null; + } + + var ruleInfoFromIssue = issueId != null ? await GetEffectiveIssueDetailsAsync(configurationScopeId, issueId.Value) : null; + + return ruleInfoFromIssue ?? await GetEffectiveRuleDetailsAsync(configurationScopeId, ruleId); + } + + private async Task GetEffectiveIssueDetailsAsync(string configurationScopeId, Guid issueId) + { + if (slCoreServiceProvider.TryGetTransientService(out IIssueSLCoreService rulesRpcService)) { try { - var ruleDetailsResponse = await rulesRpcService.GetEffectiveRuleDetailsAsync( - new GetEffectiveRuleDetailsParams(configurationScopeId, ruleId.ToString())); - return Convert(ruleDetailsResponse.details); + var issueDetailsResponse = await rulesRpcService.GetEffectiveIssueDetailsAsync( + new GetEffectiveIssueDetailsParams(configurationScopeId, issueId)); + return ruleInfoConverter.Convert(issueDetailsResponse.details); } catch (Exception e) { @@ -74,61 +80,22 @@ public async Task GetRuleInfoAsync(SonarCompositeRuleId ruleId) return null; } - private static RuleInfo Convert(EffectiveRuleDetailsDto effectiveRuleDetailsAsync) => - new(effectiveRuleDetailsAsync.key, - HtmlXmlCompatibilityHelper.EnsureHtmlIsXml(effectiveRuleDetailsAsync.description?.Left?.htmlContent), - effectiveRuleDetailsAsync.name, - Convert(effectiveRuleDetailsAsync.severityDetails.Left?.severity), - Convert(effectiveRuleDetailsAsync.severityDetails.Left?.type), - effectiveRuleDetailsAsync.description?.Right, - Convert(effectiveRuleDetailsAsync.severityDetails.Right?.cleanCodeAttribute), - Convert(effectiveRuleDetailsAsync.severityDetails.Right?.impacts)); - - private static Dictionary Convert(List cleanCodeAttribute) => - cleanCodeAttribute?.ToDictionary(x => x.softwareQuality.ToSoftwareQuality(), x => x.impactSeverity.ToSoftwareQualitySeverity()); - - - private static RuleIssueSeverity? Convert(IssueSeverity? issueSeverity) => - issueSeverity switch - { - IssueSeverity.BLOCKER => RuleIssueSeverity.Blocker, - IssueSeverity.CRITICAL => RuleIssueSeverity.Critical, - IssueSeverity.MAJOR => RuleIssueSeverity.Major, - IssueSeverity.MINOR => RuleIssueSeverity.Minor, - IssueSeverity.INFO => RuleIssueSeverity.Info, - null => null, - _ => throw new ArgumentOutOfRangeException(nameof(issueSeverity), issueSeverity, null) - }; - - private static RuleIssueType? Convert(RuleType? ruleType) => - ruleType switch + private async Task GetEffectiveRuleDetailsAsync(string configurationScopeId, SonarCompositeRuleId ruleId) + { + if (slCoreServiceProvider.TryGetTransientService(out IRulesSLCoreService rulesRpcService)) { - RuleType.CODE_SMELL => RuleIssueType.CodeSmell, - RuleType.BUG => RuleIssueType.Bug, - RuleType.VULNERABILITY => RuleIssueType.Vulnerability, - RuleType.SECURITY_HOTSPOT => RuleIssueType.Hotspot, - null => null, - _ => throw new ArgumentOutOfRangeException(nameof(ruleType), ruleType, null) - }; + try + { + var ruleDetailsResponse = await rulesRpcService.GetEffectiveRuleDetailsAsync( + new GetEffectiveRuleDetailsParams(configurationScopeId, ruleId.ToString())); + return ruleInfoConverter.Convert(ruleDetailsResponse.details); + } + catch (Exception e) + { + logger.WriteLine(e.ToString()); + } + } - private static CleanCodeAttribute? Convert(SLCore.Common.Models.CleanCodeAttribute? cleanCodeAttribute) => - cleanCodeAttribute switch - { - SLCore.Common.Models.CleanCodeAttribute.CONVENTIONAL => CleanCodeAttribute.Conventional, - SLCore.Common.Models.CleanCodeAttribute.FORMATTED => CleanCodeAttribute.Formatted, - SLCore.Common.Models.CleanCodeAttribute.IDENTIFIABLE => CleanCodeAttribute.Identifiable, - SLCore.Common.Models.CleanCodeAttribute.CLEAR => CleanCodeAttribute.Clear, - SLCore.Common.Models.CleanCodeAttribute.COMPLETE => CleanCodeAttribute.Complete, - SLCore.Common.Models.CleanCodeAttribute.EFFICIENT => CleanCodeAttribute.Efficient, - SLCore.Common.Models.CleanCodeAttribute.LOGICAL => CleanCodeAttribute.Logical, - SLCore.Common.Models.CleanCodeAttribute.DISTINCT => CleanCodeAttribute.Distinct, - SLCore.Common.Models.CleanCodeAttribute.FOCUSED => CleanCodeAttribute.Focused, - SLCore.Common.Models.CleanCodeAttribute.MODULAR => CleanCodeAttribute.Modular, - SLCore.Common.Models.CleanCodeAttribute.TESTED => CleanCodeAttribute.Tested, - SLCore.Common.Models.CleanCodeAttribute.LAWFUL => CleanCodeAttribute.Lawful, - SLCore.Common.Models.CleanCodeAttribute.RESPECTFUL => CleanCodeAttribute.Respectful, - SLCore.Common.Models.CleanCodeAttribute.TRUSTWORTHY => CleanCodeAttribute.Trustworthy, - null => null, - _ => throw new ArgumentOutOfRangeException(nameof(cleanCodeAttribute), cleanCodeAttribute, null) - }; + return null; + } } diff --git a/src/Infrastructure.VS.UnitTests/ErrorListHelperTests.cs b/src/Infrastructure.VS.UnitTests/ErrorListHelperTests.cs index 83845ae333..ec70d46d49 100644 --- a/src/Infrastructure.VS.UnitTests/ErrorListHelperTests.cs +++ b/src/Infrastructure.VS.UnitTests/ErrorListHelperTests.cs @@ -446,6 +446,44 @@ public void TryGetRuleIdAndSuppressionStateFromSelectedRow_NoSuppressionState_Re isSuppressed.Should().Be(expectedSuppression); } + [TestMethod] + public void TryGetFilterableIssue_SonarIssue_IssueReturned() + { + var issueMock = Mock.Of(); + var issueHandle = CreateIssueHandle(111, new Dictionary + { + { StandardTableKeyNames.BuildTool, "SonarLint" }, + { StandardTableKeyNames.ErrorCode, "javascript:S333"}, + { SonarLintTableControlConstants.IssueVizColumnName, issueMock } + }); + var errorList = CreateErrorList(issueHandle); + var serviceProvider = CreateServiceOperation(errorList); + var testSubject = new ErrorListHelper(serviceProvider); + + bool result = testSubject.TryGetFilterableIssue(issueHandle, out var issue); + + result.Should().BeTrue(); + issue.Should().BeSameAs(issueMock); + } + + [TestMethod] + public void TryGetFilterableIssue_NoAnalysisIssue_IssueNotReturned() + { + var issueHandle = CreateIssueHandle(111, new Dictionary + { + { StandardTableKeyNames.BuildTool, "SonarLint" }, + { 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 _); + + result.Should().BeFalse(); + } + private IVsUIServiceOperation CreateServiceOperation(IErrorList svcToPassToCallback) { var serviceOp = new Mock(); diff --git a/src/Infrastructure.VS/ErrorListHelper.cs b/src/Infrastructure.VS/ErrorListHelper.cs index 463d14a3ba..4beb5f0b80 100644 --- a/src/Infrastructure.VS/ErrorListHelper.cs +++ b/src/Infrastructure.VS/ErrorListHelper.cs @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using System.ComponentModel.Composition; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; @@ -89,8 +88,19 @@ public bool TryGetIssueFromSelectedRow(out IFilterableIssue issue) { IFilterableIssue issueOut = null; var result = vSServiceOperation.Execute( - errorList => TryGetSelectedSnapshotAndIndex(errorList, out var snapshot, out var index) - && TryGetValue(snapshot, index, SonarLintTableControlConstants.IssueVizColumnName, out issueOut)); + errorList => TryGetSelectedTableEntry(errorList, out var handle) && TryGetFilterableIssue(handle, out issueOut)); + + issue = 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; @@ -155,7 +165,7 @@ private static string FindErrorCodeForEntry(ITableEntriesSnapshot snapshot, int { return $"{SonarRuleRepoKeys.CSharpRules}:{errorCode}"; } - + if (helpLink.Contains("rules.sonarsource.com/vbnet/")) { return $"{SonarRuleRepoKeys.VBNetRules}:{errorCode}"; @@ -197,7 +207,11 @@ private static bool TryGetSelectedTableEntry(IErrorList errorList, out ITableEnt return true; } - private static bool TryGetValue(ITableEntriesSnapshot snapshot, int index, string columnName, out T value) + private static bool TryGetValue( + ITableEntriesSnapshot snapshot, + int index, + string columnName, + out T value) { value = default; diff --git a/src/Infrastructure.VS/IErrorListHelper.cs b/src/Infrastructure.VS/IErrorListHelper.cs index 5661e5eb00..1bf57fdb64 100644 --- a/src/Infrastructure.VS/IErrorListHelper.cs +++ b/src/Infrastructure.VS/IErrorListHelper.cs @@ -51,6 +51,13 @@ public interface IErrorListHelper /// True if issue is present in the selected row, False if not present or multiple rows selected bool TryGetIssueFromSelectedRow(out IFilterableIssue issue); + /// + /// Extracts, if present, from the hidden column + /// The method will only return a rule key if the row represents a Sonar analysis issue for any supported language (including Roslyn languages i.e. C# and VB.NET) + /// + /// True if issue is present in the provided row + bool TryGetFilterableIssue(ITableEntryHandle handle, out IFilterableIssue issue); + /// /// Extracts from error code, line number and file path. Does not calculate line hash. /// diff --git a/src/IssueViz.Security/Hotspots/HotspotsList/HotspotsControl.xaml b/src/IssueViz.Security/Hotspots/HotspotsList/HotspotsControl.xaml index 66f02160c2..fe0e54411a 100644 --- a/src/IssueViz.Security/Hotspots/HotspotsList/HotspotsControl.xaml +++ b/src/IssueViz.Security/Hotspots/HotspotsList/HotspotsControl.xaml @@ -99,6 +99,7 @@ + diff --git a/src/IssueViz.Security/OpenInIdeHotspots_List/HotspotsList/OpenInIDEHotspotsControl.xaml b/src/IssueViz.Security/OpenInIdeHotspots_List/HotspotsList/OpenInIDEHotspotsControl.xaml index ff53980ea3..c06d875ca5 100644 --- a/src/IssueViz.Security/OpenInIdeHotspots_List/HotspotsList/OpenInIDEHotspotsControl.xaml +++ b/src/IssueViz.Security/OpenInIdeHotspots_List/HotspotsList/OpenInIDEHotspotsControl.xaml @@ -116,6 +116,7 @@ + diff --git a/src/IssueViz.Security/Taint/TaintList/TaintIssuesControl.xaml b/src/IssueViz.Security/Taint/TaintList/TaintIssuesControl.xaml index 1b11b1a9e9..6b6a30f6c5 100644 --- a/src/IssueViz.Security/Taint/TaintList/TaintIssuesControl.xaml +++ b/src/IssueViz.Security/Taint/TaintList/TaintIssuesControl.xaml @@ -174,6 +174,7 @@ + diff --git a/src/IssueViz.UnitTests/IssueVisualizationControl/NavigateToRuleDescriptionCommandConverterTests.cs b/src/IssueViz.UnitTests/IssueVisualizationControl/NavigateToRuleDescriptionCommandConverterTests.cs index b5637c1565..2c2d648c18 100644 --- a/src/IssueViz.UnitTests/IssueVisualizationControl/NavigateToRuleDescriptionCommandConverterTests.cs +++ b/src/IssueViz.UnitTests/IssueVisualizationControl/NavigateToRuleDescriptionCommandConverterTests.cs @@ -18,8 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; using SonarLint.VisualStudio.IssueVisualization.IssueVisualizationControl.ViewModels.Commands; using SonarLint.VisualStudio.TestInfrastructure; @@ -43,13 +41,15 @@ public void Convert_CorrectFormat_Converts() } [TestMethod] - public void Convert_WrongNumberOfParams_ReturnsNull() + public void Convert_WrongNumberOfParams_IgnoresAdditionalParameter() { - var values = new object[] { "RuleKey", "context", "third param" }; + var values = new object[] { "RuleKey", "context", "third param", "fourth param" }; var command = testSubject.Convert(values, default, default, default); - command.Should().BeNull(); + (command is NavigateToRuleDescriptionCommandParam).Should().BeTrue(); + ((NavigateToRuleDescriptionCommandParam)command).FullRuleKey.Should().Be("RuleKey"); + ((NavigateToRuleDescriptionCommandParam)command).Context.Should().Be("context"); } [DataRow("str", 3)] @@ -90,5 +90,34 @@ public void ConvertBack_WrongType_ReturnsValues() values.Should().BeNull(); } } + + [TestMethod] + public void Convert_ThirdOptionalValueProvided_SetsIssueId() + { + var issuedId = Guid.NewGuid(); + var values = new object[] { "RuleKey", "context", issuedId }; + + var command = testSubject.Convert(values, default, default, default); + + var navigateToRuleDescriptionParam = command as NavigateToRuleDescriptionCommandParam; + navigateToRuleDescriptionParam.Should().NotBeNull(); + navigateToRuleDescriptionParam.FullRuleKey.Should().Be("RuleKey"); + navigateToRuleDescriptionParam.Context.Should().Be("context"); + navigateToRuleDescriptionParam.IssueId.Should().Be(issuedId); + } + + [TestMethod] + public void Convert_ThirdOptionalValueIsNull_SetsIssueIdToNull() + { + var values = new object[] { "RuleKey", "context", null }; + + var command = testSubject.Convert(values, default, default, default); + + var navigateToRuleDescriptionParam = command as NavigateToRuleDescriptionCommandParam; + navigateToRuleDescriptionParam.Should().NotBeNull(); + navigateToRuleDescriptionParam.FullRuleKey.Should().Be("RuleKey"); + navigateToRuleDescriptionParam.Context.Should().Be("context"); + navigateToRuleDescriptionParam.IssueId.Should().BeNull(); + } } } diff --git a/src/IssueViz.UnitTests/IssueVisualizationControl/ViewModelCommands/NavigateToRuleDescriptionCommandTests.cs b/src/IssueViz.UnitTests/IssueVisualizationControl/ViewModelCommands/NavigateToRuleDescriptionCommandTests.cs index e3baeedbc0..d732bbf95f 100644 --- a/src/IssueViz.UnitTests/IssueVisualizationControl/ViewModelCommands/NavigateToRuleDescriptionCommandTests.cs +++ b/src/IssueViz.UnitTests/IssueVisualizationControl/ViewModelCommands/NavigateToRuleDescriptionCommandTests.cs @@ -18,8 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.IssueVisualization.IssueVisualizationControl.ViewModels.Commands; @@ -82,7 +80,7 @@ public void Execute_RuleDocumentationShown(string fullRuleKey) testSubject.Execute(executeParam); - educationService.Verify(x => x.ShowRuleHelp(It.IsAny(), /* todo */ null), Times.Once); + educationService.Verify(x => x.ShowRuleHelp(It.IsAny(),null, /* todo by SLVS-1630 */null), Times.Once); educationService.VerifyNoOtherCalls(); var actualRuleId = (SonarCompositeRuleId)educationService.Invocations[0].Arguments[0]; @@ -103,6 +101,20 @@ public void Execute_WrongTypeParameter_DoesNotCrash() educationService.VerifyNoOtherCalls(); } + [TestMethod] + public void Execute_IssueIdProvided_RuleDocumentationShownForIssue() + { + var issueId = Guid.NewGuid(); + var educationService = new Mock(); + var testSubject = CreateTestSubject(educationService.Object); + var executeParam = new NavigateToRuleDescriptionCommandParam { FullRuleKey = "csharp:S100", IssueId = issueId}; + + testSubject.Execute(executeParam); + + educationService.Verify(x => x.ShowRuleHelp(It.IsAny(), issueId, null), Times.Once); + educationService.VerifyNoOtherCalls(); + } + private NavigateToRuleDescriptionCommand CreateTestSubject(IEducation educationService = null) { educationService ??= Mock.Of(); diff --git a/src/IssueViz.UnitTests/Models/AnalysisIssueVisualizationTests.cs b/src/IssueViz.UnitTests/Models/AnalysisIssueVisualizationTests.cs index c380357fea..93dab60f90 100644 --- a/src/IssueViz.UnitTests/Models/AnalysisIssueVisualizationTests.cs +++ b/src/IssueViz.UnitTests/Models/AnalysisIssueVisualizationTests.cs @@ -211,7 +211,9 @@ public void SetIsSuppressed_HasSubscribers_VerifyRaised() [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); @@ -223,6 +225,7 @@ public void IsFilterable() var filterable = (IFilterableIssue)testSubject; + 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); diff --git a/src/IssueViz/IssueVisualizationControl/IssueVisualizationControl.xaml b/src/IssueViz/IssueVisualizationControl/IssueVisualizationControl.xaml index f250640fd0..84a7df41b3 100644 --- a/src/IssueViz/IssueVisualizationControl/IssueVisualizationControl.xaml +++ b/src/IssueViz/IssueVisualizationControl/IssueVisualizationControl.xaml @@ -322,6 +322,7 @@ + diff --git a/src/IssueViz/IssueVisualizationControl/ViewModels/Commands/NavigateToRuleDescriptionCommand.cs b/src/IssueViz/IssueVisualizationControl/ViewModels/Commands/NavigateToRuleDescriptionCommand.cs index f9de58f3ca..a094284bd3 100644 --- a/src/IssueViz/IssueVisualizationControl/ViewModels/Commands/NavigateToRuleDescriptionCommand.cs +++ b/src/IssueViz/IssueVisualizationControl/ViewModels/Commands/NavigateToRuleDescriptionCommand.cs @@ -44,7 +44,7 @@ public NavigateToRuleDescriptionCommand(IEducation educationService) var paramObject = parameter as NavigateToRuleDescriptionCommandParam; if (SonarCompositeRuleId.TryParse(paramObject?.FullRuleKey, out var ruleId)) { - educationService.ShowRuleHelp(ruleId, paramObject?.Context); + educationService.ShowRuleHelp(ruleId, paramObject?.IssueId, paramObject?.Context); } }, parameter => parameter is NavigateToRuleDescriptionCommandParam s && @@ -56,6 +56,10 @@ public NavigateToRuleDescriptionCommand(IEducation educationService) internal class NavigateToRuleDescriptionCommandParam { + /// + /// The id of the issue that comes from SlCore + /// + public Guid? IssueId { get; set; } public string FullRuleKey { get; set; } public string Context { get; set; } } @@ -64,9 +68,14 @@ public class NavigateToRuleDescriptionCommandConverter : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { - if (values.Length == 2 && values[0] is string && (values[1] is string || values[1] == null)) + if (values.Length >= 2 && values[0] is string && (values[1] is string || values[1] == null)) { - return new NavigateToRuleDescriptionCommandParam { FullRuleKey = (string)values[0], Context = (string)values[1] }; + var parameters = new NavigateToRuleDescriptionCommandParam { FullRuleKey = (string)values[0], Context = (string)values[1] }; + if (values.Length == 3 && values[2] is Guid) + { + parameters.IssueId = (Guid)values[2]; + } + return parameters; } return null; } diff --git a/src/IssueViz/Models/AnalysisIssueVisualization.cs b/src/IssueViz/Models/AnalysisIssueVisualization.cs index 15cdfd9dae..7facf0c2e7 100644 --- a/src/IssueViz/Models/AnalysisIssueVisualization.cs +++ b/src/IssueViz/Models/AnalysisIssueVisualization.cs @@ -107,6 +107,7 @@ protected virtual void NotifyPropertyChanged([CallerMemberName] string propertyN PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } + public Guid? IssueId => Issue.Id; string IFilterableIssue.RuleId => Issue.RuleKey; string IFilterableIssue.FilePath => CurrentFilePath; diff --git a/src/SLCore.IntegrationTests/RuleDescriptionConversionSmokeTest.cs b/src/SLCore.IntegrationTests/RuleDescriptionConversionSmokeTest.cs index 5568601d39..bdfb64a2e7 100644 --- a/src/SLCore.IntegrationTests/RuleDescriptionConversionSmokeTest.cs +++ b/src/SLCore.IntegrationTests/RuleDescriptionConversionSmokeTest.cs @@ -132,6 +132,7 @@ private static SLCoreRuleMetaDataProvider CreateSlCoreRuleMetaDataProvider(SLCor IActiveConfigScopeTracker activeConfigScopeTracker, ILogger testLogger) => new(slCoreTestRunner.SLCoreServiceProvider, activeConfigScopeTracker, + new RuleInfoConverter(), testLogger); private static ActiveConfigScopeTracker CreateActiveConfigScopeTracker(SLCoreTestRunner slCoreTestRunner) => diff --git a/src/SLCore.UnitTests/Service/Issue/GetEffectiveIssueDetailsResponseTests.cs b/src/SLCore.UnitTests/Service/Issue/GetEffectiveIssueDetailsResponseTests.cs index fa33a74ce1..a8d77dfe85 100644 --- a/src/SLCore.UnitTests/Service/Issue/GetEffectiveIssueDetailsResponseTests.cs +++ b/src/SLCore.UnitTests/Service/Issue/GetEffectiveIssueDetailsResponseTests.cs @@ -34,7 +34,7 @@ public void Deserialized_AsExpected() { var expected = new GetEffectiveIssueDetailsResponse( details: new EffectiveIssueDetailsDto( - ruleKey: "S3776", + key: "S3776", name: "Cognitive Complexity of methods should not be too high", language: Language.CS, vulnerabilityProbability: VulnerabilityProbability.HIGH, diff --git a/src/SLCore/Service/Issue/Models/EffectiveIssueDetailsDto.cs b/src/SLCore/Service/Issue/Models/EffectiveIssueDetailsDto.cs index 9bce238de9..e86d9697e0 100644 --- a/src/SLCore/Service/Issue/Models/EffectiveIssueDetailsDto.cs +++ b/src/SLCore/Service/Issue/Models/EffectiveIssueDetailsDto.cs @@ -26,11 +26,11 @@ namespace SonarLint.VisualStudio.SLCore.Service.Issue.Models; public record EffectiveIssueDetailsDto( - string ruleKey, + [JsonProperty("ruleKey")] string key, string name, Language language, VulnerabilityProbability? vulnerabilityProbability, [JsonConverter(typeof(EitherJsonConverter))] Either description, [JsonProperty("params")] List parameters, [JsonConverter(typeof(EitherJsonConverter))] Either severityDetails, - string ruleDescriptionContextKey); + string ruleDescriptionContextKey) : IRuleDetails; diff --git a/src/SLCore/Service/Rules/Models/EffectiveRuleDetailsDto.cs b/src/SLCore/Service/Rules/Models/EffectiveRuleDetailsDto.cs index 25455f34bb..45b4798ecf 100644 --- a/src/SLCore/Service/Rules/Models/EffectiveRuleDetailsDto.cs +++ b/src/SLCore/Service/Rules/Models/EffectiveRuleDetailsDto.cs @@ -18,9 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Collections.Generic; using Newtonsoft.Json; -using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.SLCore.Common.Models; using SonarLint.VisualStudio.SLCore.Protocol; using Language = SonarLint.VisualStudio.SLCore.Common.Models.Language; @@ -36,8 +34,7 @@ public record EffectiveRuleDetailsDto( VulnerabilityProbability? vulnerabilityProbability, [property: JsonConverter(typeof(EitherJsonConverter))] Either description, - List @params); + [JsonProperty("params")] List parameters) : IRuleDetails; public record StandardModeDetails(IssueSeverity severity, RuleType type); - public record MQRModeDetails(CleanCodeAttribute cleanCodeAttribute, List impacts); diff --git a/src/SLCore/Service/Rules/Models/IRuleDetails.cs b/src/SLCore/Service/Rules/Models/IRuleDetails.cs new file mode 100644 index 0000000000..1f41d4a522 --- /dev/null +++ b/src/SLCore/Service/Rules/Models/IRuleDetails.cs @@ -0,0 +1,35 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.SLCore.Common.Models; +using SonarLint.VisualStudio.SLCore.Protocol; + +namespace SonarLint.VisualStudio.SLCore.Service.Rules.Models; + +public interface IRuleDetails +{ + string key { get; } + string name { get; } + Language language { get; } + Either severityDetails { get; } + VulnerabilityProbability? vulnerabilityProbability { get; } + Either description { get; } + List parameters { get; } +}