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 fe90785ec6..1fe955d39e 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 ccab06a706..79b9135055 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; }
+}