Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SLVS-1753 SLVS-1754 Send mute issue request to SlCore #6030

Merged
merged 23 commits into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
189f4f5
Remove the ServerIssuesStoreWriter from MuteIssuesService
vnaskos-sonar Feb 12, 2025
2c88754
Remove the SonarQubeService from MuteIssuesService
vnaskos-sonar Feb 12, 2025
df520c8
Change ResolveIssueWithDialogAsync method signature
vnaskos-sonar Feb 12, 2025
7fd1c02
Mute issue using the SlCore service
vnaskos-sonar Feb 13, 2025
47a1b90
Call SLCore CheckStatusChangePermittedAsync
vnaskos-sonar Feb 12, 2025
e4f2ba9
Cleanup string resources
vnaskos-sonar Feb 14, 2025
119a60a
Cleanup obsolete tests
vnaskos-sonar Feb 14, 2025
7fab227
Add test for null config scope
vnaskos-sonar Feb 14, 2025
eb66b6a
Move TryGetTransientService inside try catch
vnaskos-sonar Feb 14, 2025
3cf1707
Add more tests
vnaskos-sonar Feb 14, 2025
3369f7c
Trim empty comment
vnaskos-sonar Feb 14, 2025
a89ba9d
Add context logger
vnaskos-sonar Feb 14, 2025
cf26bfb
Catch and return false on CancelledException
vnaskos-sonar Feb 17, 2025
f483efa
Replace concrete exception types with generic and test for logs
vnaskos-sonar Feb 17, 2025
5440562
Test logger for correct context
vnaskos-sonar Feb 17, 2025
23f9c9f
Check for empty issue server key
vnaskos-sonar Feb 17, 2025
ca3cbcd
Change IssueNotFound message
vnaskos-sonar Feb 17, 2025
2b86418
CaYC: Replace Mock with NSubstitute
vnaskos-sonar Feb 17, 2025
a43379e
Add test cases for MuteIssueException behavior
vnaskos-sonar Feb 17, 2025
0751b1c
Show message when mute succeeded but comment failed
vnaskos-sonar Feb 17, 2025
4d0b6af
Show message if roslyn issue is already resolved on server
vnaskos-sonar Feb 17, 2025
f7e3fcd
Logger may be null
vnaskos-sonar Feb 18, 2025
deb950f
Fix typo
vnaskos-sonar Feb 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
371 changes: 214 additions & 157 deletions src/ConnectedMode.UnitTests/Transition/MuteIssuesServiceTests.cs

Large diffs are not rendered by default.

30 changes: 15 additions & 15 deletions src/ConnectedMode/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 10 additions & 10 deletions src/ConnectedMode/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -286,20 +286,20 @@
<data name="SharedBindingConfigProvider_SharedFolderNotFound" xml:space="preserve">
<value>[SharedBindingConfigProvider] The .sonarlint shared folder was not found</value>
</data>
<data name="MuteWindowService_NotInConnectedMode" xml:space="preserve">
<value>[Transition]Issue muting is only supported in connected mode</value>
<data name="MuteIssue_IssueNotFound" xml:space="preserve">
<value>Could not find a matching issue on the server.</value>
</data>
<data name="MuteIssuesService_Error_Caption" xml:space="preserve">
<value>Error</value>
<data name="MuteIssue_NotInConnectedMode" xml:space="preserve">
<value>Issue muting is only supported in connected mode</value>
</data>
<data name="MuteIssuesService_Error_CommentAdditionFailed" xml:space="preserve">
<value>Issue is resolved but an error occured while adding the comment, please refer to the logs for more information.</value>
<data name="MuteIssue_NotPermitted" xml:space="preserve">
<value>Issue muting was not permitted [issueKey: {0}]: {1}</value>
</data>
<data name="MuteIssuesService_Error_FailedToTransition" xml:space="preserve">
<value>Unable to resolve the issue, please refer to the logs for more information.</value>
<data name="MuteIssue_AddCommentFailed" xml:space="preserve">
<value>An error occurred while adding the comment on muted issue [issueKey: {0}]: {1}</value>
</data>
<data name="MuteIssuesService_Error_InsufficientPermissions" xml:space="preserve">
<value>Credentials you have provided do not have enough permission to resolve issues. It requires the permission 'Administer Issues'.</value>
<data name="MuteIssue_AnErrorOccurred" xml:space="preserve">
<value>An error occurred while muting issue [issueKey: {0}]: {1}</value>
</data>
<data name="ValidateCredentials_Fails" xml:space="preserve">
<value>[ConnectedMode] Validating credentials failed</value>
Expand Down
42 changes: 42 additions & 0 deletions src/ConnectedMode/Transition/MuteIssueException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* SonarLint for Visual Studio
* Copyright (C) 2016-2025 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

namespace SonarLint.VisualStudio.ConnectedMode.Transition;

public class MuteIssueException : Exception
{
private MuteIssueException()
{
}

public MuteIssueException(string message)
: base(message)
{
}

public MuteIssueException(Exception ex)
: base(ex.Message, ex.InnerException)
{
}

public class MuteIssueCancelledException : MuteIssueException;

public class MuteIssueCommentFailedException : MuteIssueException;
}
204 changes: 131 additions & 73 deletions src/ConnectedMode/Transition/MuteIssuesService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,101 +18,159 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

using System;
using System.ComponentModel.Composition;
using System.Resources;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using SonarLint.VisualStudio.ConnectedMode.Suppressions;
using SonarLint.VisualStudio.Core;
using SonarLint.VisualStudio.Core.Binding;
using SonarLint.VisualStudio.Core.ConfigurationScope;
using SonarLint.VisualStudio.Core.Transition;
using SonarLint.VisualStudio.Infrastructure.VS;
using SonarQube.Client;
using SonarLint.VisualStudio.SLCore;
using SonarLint.VisualStudio.SLCore.Core;
using SonarLint.VisualStudio.SLCore.Service.Issue;
using SonarLint.VisualStudio.SLCore.Service.Issue.Models;
using SonarQube.Client.Models;

namespace SonarLint.VisualStudio.ConnectedMode.Transition
namespace SonarLint.VisualStudio.ConnectedMode.Transition;

[Export(typeof(IMuteIssuesService))]
[PartCreationPolicy(CreationPolicy.Shared)]
[method: ImportingConstructor]
internal class MuteIssuesService(
IMuteIssuesWindowService muteIssuesWindowService,
IActiveConfigScopeTracker activeConfigScopeTracker,
ISLCoreServiceProvider slCoreServiceProvider,
ILogger logger,
IThreadHandling threadHandling)
: IMuteIssuesService
{
[Export(typeof(IMuteIssuesService))]
[PartCreationPolicy(CreationPolicy.Shared)]
internal class MuteIssuesService : IMuteIssuesService
private readonly ILogger logger = logger.ForContext(nameof(MuteIssuesService));

public async Task ResolveIssueWithDialogAsync(string issueServerKey)
{
threadHandling.ThrowIfOnUIThread();

var currentConfigScope = activeConfigScopeTracker.Current;
CheckIsInConnectedMode(currentConfigScope);
CheckIssueServerKeyNotNullOrEmpty(issueServerKey);

await GetAllowedStatusesAsync(currentConfigScope.ConnectionId, issueServerKey);
var windowResponse = await PromptMuteIssueResolutionAsync();
await MuteIssueAsync(currentConfigScope.Id, issueServerKey, windowResponse.IssueTransition);
await AddCommentAsync(currentConfigScope.Id, issueServerKey, windowResponse.Comment);
}

private async Task<MuteIssuesWindowResponse> PromptMuteIssueResolutionAsync()
{
private readonly IActiveSolutionBoundTracker activeSolutionBoundTracker;
private readonly ILogger logger;
private readonly IMuteIssuesWindowService muteIssuesWindowService;
private readonly IThreadHandling threadHandling;
private readonly ISonarQubeService sonarQubeService;
private readonly IServerIssuesStoreWriter serverIssuesStore;
private readonly IMessageBox messageBox;
private readonly ResourceManager resourceManager;

[ImportingConstructor]
public MuteIssuesService(IActiveSolutionBoundTracker activeSolutionBoundTracker, ILogger logger, IMuteIssuesWindowService muteIssuesWindowService, ISonarQubeService sonarQubeService, IServerIssuesStoreWriter serverIssuesStore)
: this(activeSolutionBoundTracker, logger, muteIssuesWindowService, sonarQubeService, serverIssuesStore, ThreadHandling.Instance, new Core.MessageBox())
{ }

internal MuteIssuesService(IActiveSolutionBoundTracker activeSolutionBoundTracker,
ILogger logger,
IMuteIssuesWindowService muteIssuesWindowService,
ISonarQubeService sonarQubeService,
IServerIssuesStoreWriter serverIssuesStore,
IThreadHandling threadHandling,
IMessageBox messageBox)
MuteIssuesWindowResponse windowResponse = null;
await threadHandling.RunOnUIThreadAsync(() => windowResponse = muteIssuesWindowService.Show());

if (windowResponse.Result)
{
this.activeSolutionBoundTracker = activeSolutionBoundTracker;
this.logger = logger;
this.muteIssuesWindowService = muteIssuesWindowService;
this.threadHandling = threadHandling;
this.sonarQubeService = sonarQubeService;
this.serverIssuesStore = serverIssuesStore;
this.messageBox = messageBox;

resourceManager = new ResourceManager(typeof(Resources));
return windowResponse;
}

public void CacheOutOfSyncResolvedIssue(SonarQubeIssue issue)
throw new MuteIssueException.MuteIssueCancelledException();
}

private void CheckIssueServerKeyNotNullOrEmpty(string issueServerKey)
{
if (issueServerKey is { Length: > 0 })
{
threadHandling.ThrowIfOnUIThread();
return;
}

if (!issue.IsResolved)
{
throw new ArgumentException("Issue should be resolved.", nameof(issue));
}
logger.WriteLine(Resources.MuteIssue_IssueNotFound);
throw new MuteIssueException(Resources.MuteIssue_IssueNotFound);
}

serverIssuesStore.AddIssues(new []{ issue }, false);
private void CheckIsInConnectedMode(Core.ConfigurationScope.ConfigurationScope currentConfigScope)
{
if (currentConfigScope is { Id: not null, ConnectionId: not null })
{
return;
}

public async Task ResolveIssueWithDialogAsync(SonarQubeIssue issue, CancellationToken token)
logger.WriteLine(Resources.MuteIssue_NotInConnectedMode);
throw new MuteIssueException(Resources.MuteIssue_NotInConnectedMode);
}

private IIssueSLCoreService GetIssueSlCoreService()
{
if (slCoreServiceProvider.TryGetTransientService(out IIssueSLCoreService issueSlCoreService))
{
threadHandling.ThrowIfOnUIThread();
return issueSlCoreService;
}

if (!activeSolutionBoundTracker.CurrentConfiguration.Mode.IsInAConnectedMode())
{
logger.LogVerbose(Resources.MuteWindowService_NotInConnectedMode);
return;
}
logger.WriteLine(SLCoreStrings.ServiceProviderNotInitialized);
throw new MuteIssueException(SLCoreStrings.ServiceProviderNotInitialized);
}

private async Task<List<ResolutionStatus>> GetAllowedStatusesAsync(string connectionId, string issueServerKey)
{
CheckStatusChangePermittedResponse response;
try
{
var issueSlCoreService = GetIssueSlCoreService();
var checkStatusChangePermittedParams = new CheckStatusChangePermittedParams(connectionId, issueServerKey);
response = await issueSlCoreService.CheckStatusChangePermittedAsync(checkStatusChangePermittedParams);
}
catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex))
{
logger.WriteLine(Resources.MuteIssue_AnErrorOccurred, issueServerKey, ex.Message);
throw new MuteIssueException(ex);
}

MuteIssuesWindowResponse windowResponse = default;
if (!response.permitted)
{
logger.WriteLine(Resources.MuteIssue_NotPermitted, issueServerKey, response.notPermittedReason);
throw new MuteIssueException(response.notPermittedReason);
}

await threadHandling.RunOnUIThreadAsync(() => windowResponse = muteIssuesWindowService.Show());
return response.allowedStatuses;
}

if (windowResponse.Result)
private async Task MuteIssueAsync(string configurationScopeId, string issueServerKey, SonarQubeIssueTransition transition)
{
try
{
var issueSlCoreService = GetIssueSlCoreService();
var newStatus = MapSonarQubeIssueTransitionToSlCoreResolutionStatus(transition);
await issueSlCoreService.ChangeStatusAsync(new ChangeIssueStatusParams
(
configurationScopeId,
issueServerKey,
newStatus,
false // Muting taints are not supported yet
));
}
catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex))
{
logger.WriteLine(Resources.MuteIssue_AnErrorOccurred, issueServerKey, ex.Message);
throw new MuteIssueException(ex);
}
}

private async Task AddCommentAsync(string configurationScopeId, string issueServerKey, string comment)
{
try
{
var issueSlCoreService = GetIssueSlCoreService();
if (comment?.Trim() is { Length: > 0 })
{
var serviceResult = await sonarQubeService.TransitionIssueAsync(issue.IssueKey, windowResponse.IssueTransition, windowResponse.Comment, token);

if (serviceResult == SonarQubeIssueTransitionResult.Success || serviceResult == SonarQubeIssueTransitionResult.CommentAdditionFailed)
{
issue.IsResolved = true;
serverIssuesStore.AddIssues(new[] { issue }, false);
}

if (serviceResult != SonarQubeIssueTransitionResult.Success)
{
// ideally, message box invocation should be moved to Mute command
messageBox.Show(resourceManager.GetString($"MuteIssuesService_Error_{serviceResult}"), Resources.MuteIssuesService_Error_Caption, MessageBoxButton.OK, MessageBoxImage.Error);
}
await issueSlCoreService.AddCommentAsync(new AddIssueCommentParams(configurationScopeId, issueServerKey, comment));
}
}
catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex))
{
logger.WriteLine(Resources.MuteIssue_AddCommentFailed, issueServerKey, ex.Message);
throw new MuteIssueException.MuteIssueCommentFailedException();
}
}

private static ResolutionStatus MapSonarQubeIssueTransitionToSlCoreResolutionStatus(SonarQubeIssueTransition sonarQubeIssueTransition) =>
sonarQubeIssueTransition switch
{
SonarQubeIssueTransition.FalsePositive => ResolutionStatus.FALSE_POSITIVE,
SonarQubeIssueTransition.WontFix => ResolutionStatus.WONT_FIX,
SonarQubeIssueTransition.Accept => ResolutionStatus.ACCEPT,
_ => throw new ArgumentOutOfRangeException(nameof(sonarQubeIssueTransition), sonarQubeIssueTransition, null)
};
}
8 changes: 1 addition & 7 deletions src/Core/Transition/IMuteIssuesService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,10 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

using System.Threading;
using System.Threading.Tasks;
using SonarQube.Client.Models;

namespace SonarLint.VisualStudio.Core.Transition
{
public interface IMuteIssuesService
{
void CacheOutOfSyncResolvedIssue(SonarQubeIssue issue);

Task ResolveIssueWithDialogAsync(SonarQubeIssue issue, CancellationToken token);
Task ResolveIssueWithDialogAsync(string issueServerKey);
}
}
Loading