From 4dfa6d2e5945a1ac72c76a92f790f2c71c720d7b Mon Sep 17 00:00:00 2001 From: Damien Urruty Date: Fri, 14 Feb 2025 09:35:26 +0100 Subject: [PATCH] SLCORE-1158 Expose a way to request a fix suggestion --- .../sonarlint/core/ConnectionManager.java | 11 +- .../sonarlint/core/ConnectionService.java | 5 +- .../core/SonarCloudActiveEnvironment.java | 4 + .../sonarlint/core/SonarCloudRegion.java | 12 +- .../core/VersionSoonUnsupportedHelper.java | 2 +- .../core/analysis/AnalysisEngineCache.java | 4 +- .../core/analysis/AnalysisService.java | 16 +- .../sonarlint/core/commons/Binding.java | 45 +-- .../core/file/ServerFilePathsProvider.java | 10 +- .../core/fs/FileExclusionService.java | 4 +- .../core/hotspot/HotspotService.java | 8 +- .../sonarlint/core/issue/IssueService.java | 30 +- .../SkippedPluginsNotifierService.java | 2 +- .../aicodefix/AiCodeFixService.java | 119 ++++++++ .../reporting/FindingReportingService.java | 4 +- .../config/ConfigurationRepository.java | 8 +- .../SonarCloudConnectionConfiguration.java | 8 +- .../SonarQubeConnectionConfiguration.java | 2 +- .../PreviouslyRaisedFindingsRepository.java | 6 + .../repository/reporting/RaisedIssue.java | 26 ++ .../sonarlint/core/rules/RulesService.java | 6 +- .../server/event/ServerEventsService.java | 4 +- .../core/spring/SonarLintSpringAppConfig.java | 4 +- .../core/storage/StorageService.java | 2 +- .../sync/HotspotSynchronizationService.java | 8 +- .../sync/IssueSynchronizationService.java | 8 +- .../core/sync/SynchronizationService.java | 4 +- .../TaintVulnerabilityTrackingService.java | 4 +- .../core/websocket/WebSocketService.java | 6 +- .../core/BindingClueProviderTests.java | 16 +- .../core/BindingSuggestionProviderTests.java | 2 +- .../core/ConnectionManagerTests.java | 2 +- ...elemetryServerAttributesProviderTests.java | 2 +- .../VersionSoonUnsupportedHelperTests.java | 2 +- .../ShowFixSuggestionRequestHandlerTests.java | 4 +- .../server/ShowIssueRequestHandlerTests.java | 6 +- .../core/hotspot/HotspotServiceTests.java | 6 +- .../config/ConfigurationRepositoryTest.java | 8 +- ...SonarCloudConnectionConfigurationTest.java | 10 +- .../SonarQubeConnectionConfigurationTest.java | 2 +- .../rpc/impl/AiCodeFixRpcServiceDelegate.java | 38 +++ .../core/rpc/impl/SonarLintRpcServerImpl.java | 6 + .../core/serverapi/EndpointParams.java | 12 +- .../sonarlint/core/serverapi/ServerApi.java | 5 + .../core/serverapi/ServerApiHelper.java | 20 +- .../AiSuggestionRequestBodyDto.java | 25 ++ .../AiSuggestionResponseBodyDto.java | 28 ++ .../fixsuggestions/FixSuggestionsApi.java | 49 ++++ .../fixsuggestions/package-info.java | 23 ++ .../MockWebServerExtensionWithProtobuf.java | 2 +- .../fixsuggestions/FixSuggestionsApiTest.java | 87 ++++++ .../MockWebServerExtensionWithProtobuf.java | 2 +- .../aicodefix/AiCodeFixMediumTest.java | 258 ++++++++++++++++++ .../MockWebServerExtensionWithProtobuf.java | 2 +- .../rpc/protocol/SonarLintRpcErrorCode.java | 2 + .../core/rpc/protocol/SonarLintRpcServer.java | 4 + .../aicodefix/AiCodeFixRpcService.java | 37 +++ .../aicodefix/SuggestFixChangeDto.java | 44 +++ .../aicodefix/SuggestFixParams.java | 40 +++ .../aicodefix/SuggestFixResponse.java | 47 ++++ .../remediation/aicodefix/package-info.java | 23 ++ .../test/utils/SonarLintTestRpcServer.java | 6 + .../core/test/utils/server/ServerFixture.java | 61 ++++- 63 files changed, 1091 insertions(+), 162 deletions(-) create mode 100644 backend/core/src/main/java/org/sonarsource/sonarlint/core/remediation/aicodefix/AiCodeFixService.java create mode 100644 backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/reporting/RaisedIssue.java create mode 100644 backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/AiCodeFixRpcServiceDelegate.java create mode 100644 backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/AiSuggestionRequestBodyDto.java create mode 100644 backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/AiSuggestionResponseBodyDto.java create mode 100644 backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/FixSuggestionsApi.java create mode 100644 backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/package-info.java create mode 100644 backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/FixSuggestionsApiTest.java create mode 100644 medium-tests/src/test/java/mediumtest/remediation/aicodefix/AiCodeFixMediumTest.java create mode 100644 rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/backend/remediation/aicodefix/AiCodeFixRpcService.java create mode 100644 rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/backend/remediation/aicodefix/SuggestFixChangeDto.java create mode 100644 rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/backend/remediation/aicodefix/SuggestFixParams.java create mode 100644 rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/backend/remediation/aicodefix/SuggestFixResponse.java create mode 100644 rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/backend/remediation/aicodefix/package-info.java diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/ConnectionManager.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/ConnectionManager.java index 1ff57c75c0..87dc670d23 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/ConnectionManager.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/ConnectionManager.java @@ -96,7 +96,7 @@ private boolean checkIfBearerIsSupported(EndpointParams params) { public ServerApi getServerApi(String baseUrl, @Nullable String organization, String token) { var isSonarCloud = sonarCloudActiveEnvironment.isSonarQubeCloud(baseUrl); - var params = new EndpointParams(baseUrl, isSonarCloud, organization); + var params = new EndpointParams(baseUrl, baseUrl, isSonarCloud, organization); var isBearerSupported = checkIfBearerIsSupported(params); return new ServerApi(params, httpClientProvider.getHttpClientWithPreemptiveAuth(token, isBearerSupported)); } @@ -115,15 +115,18 @@ private ServerApi getServerApiOrThrow(String connectionId) { * Used to do SonarCloud requests before knowing the organization */ public ServerApi getForSonarCloudNoOrg(Either credentials, SonarCloudRegion region) { - var endpointParams = new EndpointParams(sonarCloudActiveEnvironment.getUri(region).toString(), true, null); + var endpointParams = new EndpointParams(sonarCloudActiveEnvironment.getUri(region).toString(), sonarCloudActiveEnvironment.getApiUri(region).toString(), true, null); var httpClient = getClientFor(endpointParams, credentials); return new ServerApi(new ServerApiHelper(endpointParams, httpClient)); } public ServerApi getForTransientConnection(Either transientConnection) { var endpointParams = transientConnection.map( - sq -> new EndpointParams(sq.getServerUrl(), false, null), - sc -> new EndpointParams(sonarCloudActiveEnvironment.getUri(SonarCloudRegion.valueOf(sc.getRegion().toString())).toString(), true, sc.getOrganization())); + sq -> new EndpointParams(sq.getServerUrl(), null, false, null), + sc -> { + var region = SonarCloudRegion.valueOf(sc.getRegion().toString()); + return new EndpointParams(sonarCloudActiveEnvironment.getUri(region).toString(), sonarCloudActiveEnvironment.getApiUri(region).toString(), true, sc.getOrganization()); + }); var httpClient = getClientFor(endpointParams, transientConnection .map(TransientSonarQubeConnectionDto::getCredentials, TransientSonarCloudConnectionDto::getCredentials)); return new ServerApi(new ServerApiHelper(endpointParams, httpClient)); diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/ConnectionService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/ConnectionService.java index 2a94a86398..656164cee2 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/ConnectionService.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/ConnectionService.java @@ -88,8 +88,9 @@ private static SonarQubeConnectionConfiguration adapt(SonarQubeConnectionConfigu } private SonarCloudConnectionConfiguration adapt(SonarCloudConnectionConfigurationDto scDto) { - return new SonarCloudConnectionConfiguration(sonarCloudActiveEnvironment.getUri(SonarCloudRegion.valueOf(scDto.getRegion().toString())), scDto.getConnectionId(), - scDto.getOrganization(), SonarCloudRegion.valueOf(scDto.getRegion().toString()), scDto.isDisableNotifications()); + var region = SonarCloudRegion.valueOf(scDto.getRegion().toString()); + return new SonarCloudConnectionConfiguration(sonarCloudActiveEnvironment.getUri(region), sonarCloudActiveEnvironment.getApiUri(region), scDto.getConnectionId(), + scDto.getOrganization(), region, scDto.isDisableNotifications()); } private static void putAndLogIfDuplicateId(Map map, AbstractConnectionConfiguration config) { diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/SonarCloudActiveEnvironment.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/SonarCloudActiveEnvironment.java index 3c82fc0948..4a05955029 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/SonarCloudActiveEnvironment.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/SonarCloudActiveEnvironment.java @@ -43,6 +43,10 @@ public URI getUri(SonarCloudRegion region) { return alternativeUris != null ? alternativeUris.productionUri : region.getProductionUri(); } + public URI getApiUri(SonarCloudRegion region) { + return alternativeUris != null ? alternativeUris.productionUri : region.getApiProductionUri(); + } + public URI getWebSocketsEndpointUri(SonarCloudRegion region) { return alternativeUris != null ? alternativeUris.wsUri : region.getWebSocketUri(); } diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/SonarCloudRegion.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/SonarCloudRegion.java index ff9ffcd083..cedb50eb0b 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/SonarCloudRegion.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/SonarCloudRegion.java @@ -22,14 +22,16 @@ import java.net.URI; public enum SonarCloudRegion { - EU("https://sonarcloud.io", "wss://events-api.sonarcloud.io/"), - US("https://us.sonarcloud.io", "wss://events-api.us.sonarcloud.io/"); + EU("https://sonarcloud.io", "https://api.sonarcloud.io", "wss://events-api.sonarcloud.io/"), + US("https://us.sonarcloud.io", "https://api.us.sonarcloud.io", "wss://events-api.us.sonarcloud.io/"); private final URI productionUri; + private final URI apiProductionUri; private final URI webSocketUri; - SonarCloudRegion(String productionUri, String webSocketUri) { + SonarCloudRegion(String productionUri, String apiProductionUri, String webSocketUri) { this.productionUri = URI.create(productionUri); + this.apiProductionUri = URI.create(apiProductionUri); this.webSocketUri = URI.create(webSocketUri); } @@ -37,6 +39,10 @@ public URI getProductionUri() { return productionUri; } + public URI getApiProductionUri() { + return apiProductionUri; + } + public URI getWebSocketUri() { return webSocketUri; } diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/VersionSoonUnsupportedHelper.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/VersionSoonUnsupportedHelper.java index 48735feb11..353a127f6e 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/VersionSoonUnsupportedHelper.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/VersionSoonUnsupportedHelper.java @@ -87,7 +87,7 @@ private void checkIfSoonUnsupportedOncePerConnection(Set configScopeIds) configScopeIds.forEach(configScopeId -> { var effectiveBinding = configRepository.getEffectiveBinding(configScopeId); if (effectiveBinding.isPresent()) { - var connectionId = effectiveBinding.get().getConnectionId(); + var connectionId = effectiveBinding.get().connectionId(); oneConfigScopeIdPerConnection.putIfAbsent(connectionId, configScopeId); } }); diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/AnalysisEngineCache.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/AnalysisEngineCache.java index a854bdfbd7..149011b4a6 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/AnalysisEngineCache.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/AnalysisEngineCache.java @@ -83,13 +83,13 @@ public AnalysisEngineCache(ConfigurationRepository configurationRepository, Node @CheckForNull public AnalysisEngine getAnalysisEngineIfStarted(String configurationScopeId) { return configurationRepository.getEffectiveBinding(configurationScopeId) - .map(binding -> getConnectedEngineIfStarted(binding.getConnectionId())) + .map(binding -> getConnectedEngineIfStarted(binding.connectionId())) .orElseGet(this::getStandaloneEngineIfStarted); } public AnalysisEngine getOrCreateAnalysisEngine(String configurationScopeId) { return configurationRepository.getEffectiveBinding(configurationScopeId) - .map(binding -> getOrCreateConnectedEngine(binding.getConnectionId())) + .map(binding -> getOrCreateConnectedEngine(binding.connectionId())) .orElseGet(this::getOrCreateStandaloneEngine); } diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/AnalysisService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/AnalysisService.java index 8008ce9e80..8c73e5cf22 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/AnalysisService.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/AnalysisService.java @@ -262,7 +262,7 @@ public GetAnalysisConfigResponse getAnalysisConfig(String configScopeId, boolean var analysisProperties = new HashMap<>(serverProperties); analysisProperties.putAll(userAnalysisProperties); return new GetAnalysisConfigResponse(buildConnectedActiveRules(binding, hotspotsOnly), analysisProperties, nodeJsDetailsDto, - Set.copyOf(pluginsService.getConnectedPluginPaths(binding.getConnectionId()))); + Set.copyOf(pluginsService.getConnectedPluginPaths(binding.connectionId()))); }) .orElseGet(() -> new GetAnalysisConfigResponse(buildStandaloneActiveRules(), userAnalysisProperties, nodeJsDetailsDto, Set.copyOf(pluginsService.getEmbeddedPluginPaths()))); @@ -328,23 +328,23 @@ private List buildConnectedActiveRules(Binding binding, boolean h LOG.debug(" * {}: {} active rules", languageKey, ruleSet.getRules().size()); for (ServerActiveRule possiblyDeprecatedActiveRuleFromStorage : ruleSet.getRules()) { - var activeRuleFromStorage = tryConvertDeprecatedKeys(binding.getConnectionId(), possiblyDeprecatedActiveRuleFromStorage); + var activeRuleFromStorage = tryConvertDeprecatedKeys(binding.connectionId(), possiblyDeprecatedActiveRuleFromStorage); SonarLintRuleDefinition ruleOrTemplateDefinition; if (StringUtils.isNotBlank(activeRuleFromStorage.getTemplateKey())) { - ruleOrTemplateDefinition = rulesRepository.getRule(binding.getConnectionId(), activeRuleFromStorage.getTemplateKey()).orElse(null); + ruleOrTemplateDefinition = rulesRepository.getRule(binding.connectionId(), activeRuleFromStorage.getTemplateKey()).orElse(null); if (ruleOrTemplateDefinition == null) { LOG.debug("Rule {} is enabled on the server, but its template {} is not available in SonarLint", activeRuleFromStorage.getRuleKey(), activeRuleFromStorage.getTemplateKey()); continue; } } else { - ruleOrTemplateDefinition = rulesRepository.getRule(binding.getConnectionId(), activeRuleFromStorage.getRuleKey()).orElse(null); + ruleOrTemplateDefinition = rulesRepository.getRule(binding.connectionId(), activeRuleFromStorage.getRuleKey()).orElse(null); if (ruleOrTemplateDefinition == null) { LOG.debug("Rule {} is enabled on the server, but not available in SonarLint", activeRuleFromStorage.getRuleKey()); continue; } } - if (shouldIncludeRuleForAnalysis(binding.getConnectionId(), ruleOrTemplateDefinition, hotspotsOnly)) { + if (shouldIncludeRuleForAnalysis(binding.connectionId(), ruleOrTemplateDefinition, hotspotsOnly)) { result.add(buildActiveRuleDto(ruleOrTemplateDefinition, activeRuleFromStorage)); } } @@ -618,7 +618,7 @@ private boolean isReadyForAnalysis(String configScopeId) { } private boolean isReadyForAnalysis(Binding binding) { - var pluginsValid = storageService.connection(binding.getConnectionId()).plugins().isValid(); + var pluginsValid = storageService.connection(binding.connectionId()).plugins().isValid(); var bindingStorage = storageService.binding(binding); var analyzerConfigValid = bindingStorage.analyzerConfiguration().isValid(); var findingsStorageValid = bindingStorage.findings().wasEverUpdated(); @@ -627,7 +627,7 @@ private boolean isReadyForAnalysis(Binding binding) { // this is not strictly for analysis but for tracking && findingsStorageValid; LOG.debug("isReadyForAnalysis(connectionId: {}, sonarProjectKey: {}, plugins: {}, analyzer config: {}, findings: {}) => {}", - binding.getConnectionId(), binding.getSonarProjectKey(), pluginsValid, analyzerConfigValid, findingsStorageValid, isReady); + binding.connectionId(), binding.sonarProjectKey(), pluginsValid, analyzerConfigValid, findingsStorageValid, isReady); return isReady; } @@ -636,7 +636,7 @@ public boolean shouldUseEnterpriseCSharpAnalyzer(String configurationScopeId) { if (binding.isEmpty()) { return false; } else { - var connectionId = binding.get().getConnectionId(); + var connectionId = binding.get().connectionId(); return pluginsService.shouldUseEnterpriseCSharpAnalyzer(connectionId); } } diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/commons/Binding.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/commons/Binding.java index 55d34ea6ed..2c79e6d300 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/commons/Binding.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/commons/Binding.java @@ -19,48 +19,5 @@ */ package org.sonarsource.sonarlint.core.commons; -import java.util.Objects; - -public class Binding { - - private final String connectionId; - private final String sonarProjectKey; - - public Binding(String connectionId, String sonarProjectKey) { - this.connectionId = connectionId; - this.sonarProjectKey = sonarProjectKey; - } - - public String getConnectionId() { - return connectionId; - } - - public String getSonarProjectKey() { - return sonarProjectKey; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - var binding = (Binding) o; - return Objects.equals(connectionId, binding.connectionId) && Objects.equals(sonarProjectKey, binding.sonarProjectKey); - } - - @Override - public int hashCode() { - return Objects.hash(connectionId, sonarProjectKey); - } - - @Override - public String toString() { - return "Binding{" + - "connectionId='" + connectionId + '\'' + - ", sonarProjectKey='" + sonarProjectKey + '\'' + - '}'; - } +public record Binding(String connectionId, String sonarProjectKey) { } diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/file/ServerFilePathsProvider.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/file/ServerFilePathsProvider.java index ba1d855805..bbc457130d 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/file/ServerFilePathsProvider.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/file/ServerFilePathsProvider.java @@ -98,21 +98,21 @@ private Optional> getPathsFromFileCache(Binding binding) { } private Optional> fetchPathsFromServer(Binding binding, SonarLintCancelMonitor cancelMonitor) { - var connectionOpt = connectionManager.tryGetConnection(binding.getConnectionId()); + var connectionOpt = connectionManager.tryGetConnection(binding.connectionId()); if (connectionOpt.isEmpty()) { - LOG.debug("Connection '{}' does not exist", binding.getConnectionId()); + LOG.debug("Connection '{}' does not exist", binding.connectionId()); return Optional.empty(); } try { - return connectionManager.withValidConnectionFlatMapOptionalAndReturn(binding.getConnectionId(), serverApi -> { - List paths = fetchPathsFromServer(serverApi, binding.getSonarProjectKey(), cancelMonitor); + return connectionManager.withValidConnectionFlatMapOptionalAndReturn(binding.connectionId(), serverApi -> { + List paths = fetchPathsFromServer(serverApi, binding.sonarProjectKey(), cancelMonitor); cacheServerPaths(binding, paths); return Optional.of(paths); }); } catch (CancellationException e) { throw e; } catch (Exception e) { - LOG.debug("Error while getting server file paths for project '{}'", binding.getSonarProjectKey(), e); + LOG.debug("Error while getting server file paths for project '{}'", binding.sonarProjectKey(), e); return Optional.empty(); } } diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/fs/FileExclusionService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/fs/FileExclusionService.java index ab03c248fa..d3796546ce 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/fs/FileExclusionService.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/fs/FileExclusionService.java @@ -101,10 +101,10 @@ public boolean computeIfExcluded(URI fileUri, SonarLintCancelMonitor cancelMonit if (effectiveBindingOpt.isEmpty()) { return false; } - var storage = storageService.connection(effectiveBindingOpt.get().getConnectionId()); + var storage = storageService.connection(effectiveBindingOpt.get().connectionId()); AnalyzerConfiguration analyzerConfig; try { - analyzerConfig = storage.project(effectiveBindingOpt.get().getSonarProjectKey()).analyzerConfiguration().read(); + analyzerConfig = storage.project(effectiveBindingOpt.get().sonarProjectKey()).analyzerConfiguration().read(); } catch (StorageException e) { LOG.debug("Unable to read settings in local storage", e); return false; diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/hotspot/HotspotService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/hotspot/HotspotService.java index b47c89473c..4fd350d6e0 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/hotspot/HotspotService.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/hotspot/HotspotService.java @@ -82,7 +82,7 @@ public HotspotService(SonarLintRpcClient client, StorageService storageService, public void openHotspotInBrowser(String configScopeId, String hotspotKey) { var effectiveBinding = configurationRepository.getEffectiveBinding(configScopeId); - var endpointParams = effectiveBinding.flatMap(binding -> connectionRepository.getEndpointParams(binding.getConnectionId())); + var endpointParams = effectiveBinding.flatMap(binding -> connectionRepository.getEndpointParams(binding.connectionId())); if (effectiveBinding.isEmpty() || endpointParams.isEmpty()) { LOG.warn("Configuration scope {} is not bound properly, unable to open hotspot", configScopeId); return; @@ -93,7 +93,7 @@ public void openHotspotInBrowser(String configScopeId, String hotspotKey) { return; } - var url = buildHotspotUrl(effectiveBinding.get().getSonarProjectKey(), branchName.get(), hotspotKey, endpointParams.get()); + var url = buildHotspotUrl(effectiveBinding.get().sonarProjectKey(), branchName.get(), hotspotKey, endpointParams.get()); client.openUrlInBrowser(new OpenUrlInBrowserParams(url)); @@ -110,7 +110,7 @@ public CheckLocalDetectionSupportedResponse checkLocalDetectionSupported(String if (effectiveBinding.isEmpty()) { return new CheckLocalDetectionSupportedResponse(false, NO_BINDING_REASON); } - var connectionId = effectiveBinding.get().getConnectionId(); + var connectionId = effectiveBinding.get().connectionId(); if (connectionRepository.getConnectionById(connectionId) == null) { var error = new ResponseError(SonarLintRpcErrorCode.CONNECTION_NOT_FOUND, "The provided configuration scope is bound to an unknown connection: " + connectionId, connectionId); @@ -146,7 +146,7 @@ public void changeStatus(String configurationScopeId, String hotspotKey, Hotspot LOG.debug("No binding for config scope {}", configurationScopeId); return; } - connectionManager.withValidConnection(effectiveBindingOpt.get().getConnectionId(), serverApi -> { + connectionManager.withValidConnection(effectiveBindingOpt.get().connectionId(), serverApi -> { serverApi.hotspot().changeStatus(hotspotKey, newStatus, cancelMonitor); saveStatusInStorage(effectiveBindingOpt.get(), hotspotKey, newStatus); telemetryService.hotspotStatusChanged(); diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/issue/IssueService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/issue/IssueService.java index 89b5cf8577..02d180837c 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/issue/IssueService.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/issue/IssueService.java @@ -126,14 +126,14 @@ public IssueService(ConfigurationRepository configurationRepository, ConnectionM public void changeStatus(String configurationScopeId, String issueKey, ResolutionStatus newStatus, boolean isTaintIssue, SonarLintCancelMonitor cancelMonitor) { var binding = configurationRepository.getEffectiveBindingOrThrow(configurationScopeId); - var serverConnection = connectionManager.getConnectionOrThrow(binding.getConnectionId()); + var serverConnection = connectionManager.getConnectionOrThrow(binding.connectionId()); var reviewStatus = transitionByResolutionStatus.get(newStatus); var projectServerIssueStore = storageService.binding(binding).findings(); boolean isServerIssue = projectServerIssueStore.containsIssue(issueKey); if (isServerIssue) { serverConnection.withClientApi(serverApi -> serverApi.issue().changeStatus(issueKey, reviewStatus, cancelMonitor)); projectServerIssueStore.updateIssueResolutionStatus(issueKey, isTaintIssue, true) - .ifPresent(issue -> eventPublisher.publishEvent(new ServerIssueStatusChangedEvent(binding.getConnectionId(), binding.getSonarProjectKey(), issue))); + .ifPresent(issue -> eventPublisher.publishEvent(new ServerIssueStatusChangedEvent(binding.connectionId(), binding.sonarProjectKey(), issue))); } else { var localIssueOpt = asUUID(issueKey) .flatMap(localOnlyIssueRepository::findByKey); @@ -145,7 +145,7 @@ public void changeStatus(String configurationScopeId, String issueKey, Resolutio issue.resolve(coreStatus); var localOnlyIssueStore = localOnlyIssueStorageService.get(); serverConnection.withClientApi(serverApi -> serverApi.issue() - .anticipatedTransitions(binding.getSonarProjectKey(), concat(localOnlyIssueStore.loadAll(configurationScopeId), issue), cancelMonitor)); + .anticipatedTransitions(binding.sonarProjectKey(), concat(localOnlyIssueStore.loadAll(configurationScopeId), issue), cancelMonitor)); localOnlyIssueStore.storeLocalOnlyIssue(configurationScopeId, issue); eventPublisher.publishEvent(new LocalOnlyIssueStatusChangedEvent(issue)); } @@ -163,8 +163,8 @@ private static List subtract(List allIssues, Lis public boolean checkAnticipatedStatusChangeSupported(String configScopeId) { var binding = configurationRepository.getEffectiveBindingOrThrow(configScopeId); - var connectionId = binding.getConnectionId(); - return connectionManager.getConnectionOrThrow(binding.getConnectionId()) + var connectionId = binding.connectionId(); + return connectionManager.getConnectionOrThrow(binding.connectionId()) .withClientApiAndReturn(serverApi -> checkAnticipatedStatusChangeSupported(serverApi, connectionId)); } @@ -260,7 +260,7 @@ public boolean reopenIssue(String configurationScopeId, String issueId, boolean var projectServerIssueStore = storageService.binding(binding).findings(); boolean isServerIssue = projectServerIssueStore.containsIssue(issueId); if (isServerIssue) { - return connectionManager.getConnectionOrThrow(binding.getConnectionId()) + return connectionManager.getConnectionOrThrow(binding.connectionId()) .withClientApiAndReturn(serverApi -> reopenServerIssue(serverApi, binding, issueId, projectServerIssueStore, isTaintIssue, cancelMonitor)); } else { return reopenLocalIssue(issueId, configurationScopeId, cancelMonitor); @@ -281,8 +281,8 @@ private void removeAllIssuesForFile(XodusLocalOnlyIssueStore localOnlyIssueStore var issuesForFile = localOnlyIssueStore.loadForFile(configurationScopeId, filePath); var issuesToSync = subtract(allIssues, issuesForFile); var binding = configurationRepository.getEffectiveBindingOrThrow(configurationScopeId); - connectionManager.getConnectionOrThrow(binding.getConnectionId()) - .withClientApi(serverApi -> serverApi.issue().anticipatedTransitions(binding.getSonarProjectKey(), issuesToSync, cancelMonitor)); + connectionManager.getConnectionOrThrow(binding.connectionId()) + .withClientApi(serverApi -> serverApi.issue().anticipatedTransitions(binding.sonarProjectKey(), issuesToSync, cancelMonitor)); } private void removeIssueOnServer(XodusLocalOnlyIssueStore localOnlyIssueStore, @@ -290,8 +290,8 @@ private void removeIssueOnServer(XodusLocalOnlyIssueStore localOnlyIssueStore, var allIssues = localOnlyIssueStore.loadAll(configurationScopeId); var issuesToSync = allIssues.stream().filter(it -> !it.getId().equals(issueId)).toList(); var binding = configurationRepository.getEffectiveBindingOrThrow(configurationScopeId); - connectionManager.getConnectionOrThrow(binding.getConnectionId()) - .withClientApi(serverApi -> serverApi.issue().anticipatedTransitions(binding.getSonarProjectKey(), issuesToSync, cancelMonitor)); + connectionManager.getConnectionOrThrow(binding.connectionId()) + .withClientApi(serverApi -> serverApi.issue().anticipatedTransitions(binding.sonarProjectKey(), issuesToSync, cancelMonitor)); } private void setCommentOnLocalOnlyIssue(String configurationScopeId, UUID issueId, String comment, SonarLintCancelMonitor cancelMonitor) { @@ -305,8 +305,8 @@ private void setCommentOnLocalOnlyIssue(String configurationScopeId, UUID issueI var issuesToSync = localOnlyIssueStore.loadAll(configurationScopeId); issuesToSync.replaceAll(issue -> issue.getId().equals(issueId) ? commentedIssue : issue); var binding = configurationRepository.getEffectiveBindingOrThrow(configurationScopeId); - connectionManager.getConnectionOrThrow(binding.getConnectionId()) - .withClientApi(serverApi -> serverApi.issue().anticipatedTransitions(binding.getSonarProjectKey(), issuesToSync, cancelMonitor)); + connectionManager.getConnectionOrThrow(binding.connectionId()) + .withClientApi(serverApi -> serverApi.issue().anticipatedTransitions(binding.sonarProjectKey(), issuesToSync, cancelMonitor)); localOnlyIssueStore.storeLocalOnlyIssue(configurationScopeId, commentedIssue); } } else { @@ -321,7 +321,7 @@ private static ResponseErrorException issueNotFoundException(String issueId) { private void addCommentOnServerIssue(String configurationScopeId, String issueKey, String comment, SonarLintCancelMonitor cancelMonitor) { var binding = configurationRepository.getEffectiveBindingOrThrow(configurationScopeId); - connectionManager.getConnectionOrThrow(binding.getConnectionId()) + connectionManager.getConnectionOrThrow(binding.connectionId()) .withClientApi(serverApi -> serverApi.issue().addComment(issueKey, comment, cancelMonitor)); } @@ -329,7 +329,7 @@ private boolean reopenServerIssue(ServerApi connection, Binding binding, String SonarLintCancelMonitor cancelMonitor) { connection.issue().changeStatus(issueId, Transition.REOPEN, cancelMonitor); var serverIssue = projectServerIssueStore.updateIssueResolutionStatus(issueId, isTaintIssue, false); - serverIssue.ifPresent(issue -> eventPublisher.publishEvent(new ServerIssueStatusChangedEvent(binding.getConnectionId(), binding.getSonarProjectKey(), issue))); + serverIssue.ifPresent(issue -> eventPublisher.publishEvent(new ServerIssueStatusChangedEvent(binding.connectionId(), binding.sonarProjectKey(), issue))); return true; } @@ -349,7 +349,7 @@ public EffectiveIssueDetailsDto getEffectiveIssueDetails(String configurationSco var effectiveBinding = configurationRepository.getEffectiveBinding(configurationScopeId); String connectionId = null; if (effectiveBinding.isPresent()) { - connectionId = effectiveBinding.get().getConnectionId(); + connectionId = effectiveBinding.get().connectionId(); } var isMQRMode = severityModeService.isMQRModeForConnection(connectionId); var newCodeDefinition = newCodeService.getFullNewCodeDefinition(configurationScopeId).orElseGet(NewCodeDefinition::withAlwaysNew); diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/skipped/SkippedPluginsNotifierService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/skipped/SkippedPluginsNotifierService.java index ecda2c7e8b..d40177ddc6 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/skipped/SkippedPluginsNotifierService.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/skipped/SkippedPluginsNotifierService.java @@ -85,7 +85,7 @@ private List getSkippedPluginsToNotify(String configurationScopeI @CheckForNull private List getSkippedPlugins(String configurationScopeId) { return configurationRepository.getEffectiveBinding(configurationScopeId) - .map(binding -> skippedPluginsRepository.getSkippedPlugins(binding.getConnectionId())) + .map(binding -> skippedPluginsRepository.getSkippedPlugins(binding.connectionId())) .orElseGet(skippedPluginsRepository::getSkippedEmbeddedPlugins); } } diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/remediation/aicodefix/AiCodeFixService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/remediation/aicodefix/AiCodeFixService.java new file mode 100644 index 0000000000..3bc2a66bfa --- /dev/null +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/remediation/aicodefix/AiCodeFixService.java @@ -0,0 +1,119 @@ +/* + * SonarLint Core - Implementation + * 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. + */ +package org.sonarsource.sonarlint.core.remediation.aicodefix; + +import java.util.UUID; +import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; +import org.sonarsource.sonarlint.core.ConnectionManager; +import org.sonarsource.sonarlint.core.commons.Binding; +import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; +import org.sonarsource.sonarlint.core.fs.ClientFileSystemService; +import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; +import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; +import org.sonarsource.sonarlint.core.repository.connection.SonarCloudConnectionConfiguration; +import org.sonarsource.sonarlint.core.repository.reporting.PreviouslyRaisedFindingsRepository; +import org.sonarsource.sonarlint.core.repository.reporting.RaisedIssue; +import org.sonarsource.sonarlint.core.rpc.protocol.backend.remediation.aicodefix.SuggestFixChangeDto; +import org.sonarsource.sonarlint.core.rpc.protocol.backend.remediation.aicodefix.SuggestFixResponse; +import org.sonarsource.sonarlint.core.serverapi.fixsuggestions.AiSuggestionRequestBodyDto; +import org.sonarsource.sonarlint.core.serverapi.fixsuggestions.AiSuggestionResponseBodyDto; + +import static java.util.Objects.requireNonNull; +import static org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode.CONFIG_SCOPE_NOT_BOUND; +import static org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode.CONNECTION_KIND_NOT_SUPPORTED; +import static org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode.CONNECTION_NOT_FOUND; +import static org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode.FILE_NOT_FOUND; +import static org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode.ISSUE_NOT_FOUND; + +public class AiCodeFixService { + private final ConnectionConfigurationRepository connectionRepository; + private final ConfigurationRepository configurationRepository; + private final ConnectionManager connectionManager; + private final PreviouslyRaisedFindingsRepository previouslyRaisedFindingsRepository; + private final ClientFileSystemService clientFileSystemService; + + public AiCodeFixService(ConnectionConfigurationRepository connectionRepository, ConfigurationRepository configurationRepository, ConnectionManager connectionManager, + PreviouslyRaisedFindingsRepository previouslyRaisedFindingsRepository, ClientFileSystemService clientFileSystemService) { + this.connectionRepository = connectionRepository; + this.configurationRepository = configurationRepository; + this.connectionManager = connectionManager; + this.previouslyRaisedFindingsRepository = previouslyRaisedFindingsRepository; + this.clientFileSystemService = clientFileSystemService; + } + + public SuggestFixResponse suggestFix(String configurationScopeId, UUID issueId, SonarLintCancelMonitor cancelMonitor) { + var sonarQubeCloudBinding = ensureBoundToSonarQubeCloud(configurationScopeId); + var connection = connectionManager.getConnectionOrThrow(sonarQubeCloudBinding.binding().connectionId()); + var responseBodyDto = connection.withClientApiAndReturn(serverApi -> previouslyRaisedFindingsRepository.findRaisedIssueById(issueId) + .map(issue -> { + if (!isFixable(issue)) { + throw new ResponseErrorException(new ResponseError(ResponseErrorCode.InvalidParams, "The provided issue cannot be fixed", issueId)); + } + return serverApi.fixSuggestions().getAiSuggestion(toDto(sonarQubeCloudBinding.organizationKey, sonarQubeCloudBinding.binding().sonarProjectKey(), issue), + cancelMonitor); + }) + .orElseThrow(() -> new ResponseErrorException(new ResponseError(ISSUE_NOT_FOUND, "The provided issue does not exist", issueId)))); + return adapt(responseBodyDto); + } + + private static boolean isFixable(RaisedIssue issue) { + return issue.issueDto().getTextRange() != null; + } + + private static SuggestFixResponse adapt(AiSuggestionResponseBodyDto responseBodyDto) { + return new SuggestFixResponse(responseBodyDto.id(), responseBodyDto.explanation(), + responseBodyDto.changes().stream().map(change -> new SuggestFixChangeDto(change.startLine(), change.endLine(), change.newCode())).toList()); + } + + private SonarQubeCloudBinding ensureBoundToSonarQubeCloud(String configurationScopeId) { + var effectiveBinding = configurationRepository.getEffectiveBinding(configurationScopeId); + if (effectiveBinding.isEmpty()) { + throw new ResponseErrorException(new ResponseError(CONFIG_SCOPE_NOT_BOUND, "The provided configuration scope is not bound", configurationScopeId)); + } + var binding = effectiveBinding.get(); + var connection = connectionRepository.getConnectionById(binding.connectionId()); + if (connection == null) { + throw new ResponseErrorException(new ResponseError(CONNECTION_NOT_FOUND, "The provided configuration scope is bound to an unknown connection", configurationScopeId)); + } + if (!(connection instanceof SonarCloudConnectionConfiguration sonarCloudConnection)) { + throw new ResponseErrorException(new ResponseError(CONNECTION_KIND_NOT_SUPPORTED, "The provided configuration scope is not bound to SonarQube Cloud", null)); + } + return new SonarQubeCloudBinding(sonarCloudConnection.getOrganization(), binding); + } + + private AiSuggestionRequestBodyDto toDto(String organizationKey, String projectKey, RaisedIssue raisedIssue) { + // this is not perfect, the file content might have changed since the issue was detected + var clientFile = clientFileSystemService.getClientFile(raisedIssue.fileUri()); + if (clientFile == null) { + throw new ResponseErrorException(new ResponseError(FILE_NOT_FOUND, "The provided issue ID corresponds to an unknown file", null)); + } + var issue = raisedIssue.issueDto(); + // the text range presence was checked earlier + var textRange = requireNonNull(issue.getTextRange()); + return new AiSuggestionRequestBodyDto(organizationKey, projectKey, + new AiSuggestionRequestBodyDto.Issue(issue.getPrimaryMessage(), textRange.getStartLine(), textRange.getEndLine(), issue.getRuleKey(), + clientFile.getContent())); + } + + private record SonarQubeCloudBinding(String organizationKey, Binding binding) { + } +} diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/reporting/FindingReportingService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/reporting/FindingReportingService.java index 5a0a00cd10..e74e42120d 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/reporting/FindingReportingService.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/reporting/FindingReportingService.java @@ -112,7 +112,7 @@ public void streamIssue(String configurationScopeId, UUID analysisId, TrackedIss } private void triggerStreaming(String configurationScopeId, UUID analysisId) { - var connectionId = configurationRepository.getEffectiveBinding(configurationScopeId).map(Binding::getConnectionId).orElse(null); + var connectionId = configurationRepository.getEffectiveBinding(configurationScopeId).map(Binding::connectionId).orElse(null); var newCodeDefinition = newCodeService.getFullNewCodeDefinition(configurationScopeId).orElseGet(NewCodeDefinition::withAlwaysNew); var isMQRMode = severityModeService.isMQRModeForConnection(connectionId); var issuesToRaise = issuesPerFileUri.entrySet().stream() @@ -129,7 +129,7 @@ private void triggerStreaming(String configurationScopeId, UUID analysisId) { public void reportTrackedFindings(String configurationScopeId, UUID analysisId, Map> issuesToReport, Map> hotspotsToReport) { // stop streaming now, we will raise all issues one last time from this method stopStreaming(configurationScopeId); - var connectionId = configurationRepository.getEffectiveBinding(configurationScopeId).map(Binding::getConnectionId).orElse(null); + var connectionId = configurationRepository.getEffectiveBinding(configurationScopeId).map(Binding::connectionId).orElse(null); var newCodeDefinition = newCodeService.getFullNewCodeDefinition(configurationScopeId).orElseGet(NewCodeDefinition::withAlwaysNew); var isMQRMode = severityModeService.isMQRModeForConnection(connectionId); var issuesToRaise = getIssuesToRaise(issuesToReport, newCodeDefinition, isMQRMode); diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/config/ConfigurationRepository.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/config/ConfigurationRepository.java index bbf5bda6a7..2aed0f0c84 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/config/ConfigurationRepository.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/config/ConfigurationRepository.java @@ -151,8 +151,8 @@ public Collection getAllBoundScopes() { .stream() .map(scopeId -> { var effectiveBinding = getEffectiveBinding(scopeId); - return effectiveBinding.map(binding -> new BoundScope(scopeId, requireNonNull(binding.getConnectionId()), - requireNonNull(binding.getSonarProjectKey()))).orElse(null); + return effectiveBinding.map(binding -> new BoundScope(scopeId, requireNonNull(binding.connectionId()), + requireNonNull(binding.sonarProjectKey()))).orElse(null); }) .filter(Objects::nonNull) .toList(); @@ -170,8 +170,8 @@ public Collection getAllBindableUnboundScopes() { @CheckForNull public BoundScope getBoundScope(String configScopeId) { var effectiveBinding = getEffectiveBinding(configScopeId); - return effectiveBinding.map(binding -> new BoundScope(configScopeId, requireNonNull(binding.getConnectionId()), - requireNonNull(binding.getSonarProjectKey()))).orElse(null); + return effectiveBinding.map(binding -> new BoundScope(configScopeId, requireNonNull(binding.connectionId()), + requireNonNull(binding.sonarProjectKey()))).orElse(null); } diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/connection/SonarCloudConnectionConfiguration.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/connection/SonarCloudConnectionConfiguration.java index 5d35bdf5c5..32adc25653 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/connection/SonarCloudConnectionConfiguration.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/connection/SonarCloudConnectionConfiguration.java @@ -26,13 +26,17 @@ import org.sonarsource.sonarlint.core.commons.ConnectionKind; import org.sonarsource.sonarlint.core.serverapi.EndpointParams; +import static org.apache.commons.lang.StringUtils.removeEnd; + public class SonarCloudConnectionConfiguration extends AbstractConnectionConfiguration { + private final URI apiUri; private final String organization; private final SonarCloudRegion region; - public SonarCloudConnectionConfiguration(URI uri, String connectionId, String organization, SonarCloudRegion region, boolean disableNotifications) { + public SonarCloudConnectionConfiguration(URI uri, URI apiUri, String connectionId, String organization, SonarCloudRegion region, boolean disableNotifications) { super(connectionId, ConnectionKind.SONARCLOUD, disableNotifications, uri.toString()); + this.apiUri = apiUri; this.organization = organization; this.region = region; } @@ -43,7 +47,7 @@ public String getOrganization() { @Override public EndpointParams getEndpointParams() { - return new EndpointParams(getUrl(), true, organization); + return new EndpointParams(getUrl(), removeEnd(apiUri.toString(), "/"), true, organization); } public SonarCloudRegion getRegion() { diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/connection/SonarQubeConnectionConfiguration.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/connection/SonarQubeConnectionConfiguration.java index fde5735a3f..9244d3cb40 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/connection/SonarQubeConnectionConfiguration.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/connection/SonarQubeConnectionConfiguration.java @@ -30,6 +30,6 @@ public SonarQubeConnectionConfiguration(String connectionId, String serverUrl, b @Override public EndpointParams getEndpointParams() { - return new EndpointParams(getUrl(), false, null); + return new EndpointParams(getUrl(), null, false, null); } } diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/reporting/PreviouslyRaisedFindingsRepository.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/reporting/PreviouslyRaisedFindingsRepository.java index 1cd9723b65..dc6c281586 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/reporting/PreviouslyRaisedFindingsRepository.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/reporting/PreviouslyRaisedFindingsRepository.java @@ -84,4 +84,10 @@ public Optional getRaisedHotspotWithScopeAndId(String scopeId, .findFirst(); } + public Optional findRaisedIssueById(UUID issueId) { + return previouslyRaisedIssuesByScopeId.values().stream() + .flatMap(issuesByUri -> issuesByUri.entrySet().stream() + .flatMap(entry -> entry.getValue().stream().filter(issue -> issue.getId().equals(issueId)).findFirst().map(issue -> new RaisedIssue(entry.getKey(), issue)).stream())) + .findFirst(); + } } diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/reporting/RaisedIssue.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/reporting/RaisedIssue.java new file mode 100644 index 0000000000..34046ee729 --- /dev/null +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/reporting/RaisedIssue.java @@ -0,0 +1,26 @@ +/* + * SonarLint Core - Implementation + * 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. + */ +package org.sonarsource.sonarlint.core.repository.reporting; + +import java.net.URI; +import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedIssueDto; + +public record RaisedIssue(URI fileUri, RaisedIssueDto issueDto) { +} diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/rules/RulesService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/rules/RulesService.java index 8d629da91c..0e3cd25dc6 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/rules/RulesService.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/rules/RulesService.java @@ -137,7 +137,7 @@ public RuleDetails getRuleDetails(String configurationScopeId, String ruleKey, S } public RuleDetails getActiveRuleForBinding(String ruleKey, Binding binding, SonarLintCancelMonitor cancelMonitor) { - var connectionId = binding.getConnectionId(); + var connectionId = binding.connectionId(); connectionManager.getConnectionOrThrow(connectionId); var serverUsesStandardSeverityMode = !severityModeService.isMQRModeForConnection(connectionId); @@ -161,7 +161,7 @@ private Optional findServerActiveRuleInStorage(Binding binding return analyzerConfiguration.getRuleSetByLanguageKey().values().stream() .flatMap(s -> s.getRules().stream()) // XXX is it important to migrate the rule repos in tryConvertDeprecatedKeys? - .filter(r -> tryConvertDeprecatedKeys(r, binding.getConnectionId()).getRuleKey().equals(ruleKey)).findFirst(); + .filter(r -> tryConvertDeprecatedKeys(r, binding.connectionId()).getRuleKey().equals(ruleKey)).findFirst(); } private RuleDetails hydrateDetailsWithServer(String connectionId, ServerActiveRule activeRuleFromStorage, boolean skipCleanCodeTaxonomy, SonarLintCancelMonitor cancelMonitor) { @@ -401,7 +401,7 @@ private RuleDetailsForAnalysis getRuleDetailsForConnectedAnalysis(Binding bindin if (StringUtils.isNotBlank(activeRule.getTemplateKey())) { actualRuleKey = activeRule.getTemplateKey(); } - var ruleDefinitionOpt = rulesRepository.getRule(binding.getConnectionId(), actualRuleKey); + var ruleDefinitionOpt = rulesRepository.getRule(binding.connectionId(), actualRuleKey); if (ruleDefinitionOpt.isEmpty()) { throw new RuleNotFoundException(COULD_NOT_FIND_RULE + actualRuleKey + IN_EMBEDDED_RULES, actualRuleKey); } diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/server/event/ServerEventsService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/server/event/ServerEventsService.java index c02de5d7b7..f158682cd5 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/server/event/ServerEventsService.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/server/event/ServerEventsService.java @@ -161,13 +161,13 @@ private void subscribeAll(Set configurationScopeIds) { configurationScopeIds.stream() .map(configurationRepository::getConfiguredBinding) .flatMap(Optional::stream) - .collect(Collectors.groupingBy(Binding::getConnectionId, mapping(Binding::getSonarProjectKey, toSet()))) + .collect(Collectors.groupingBy(Binding::connectionId, mapping(Binding::sonarProjectKey, toSet()))) .forEach(this::subscribe); } private void subscribe(String scopeId) { configurationRepository.getConfiguredBinding(scopeId) - .ifPresent(binding -> subscribe(binding.getConnectionId(), Set.of(binding.getSonarProjectKey()))); + .ifPresent(binding -> subscribe(binding.connectionId(), Set.of(binding.sonarProjectKey()))); } private void subscribe(String connectionId, Set possiblyNewProjectKeys) { diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/spring/SonarLintSpringAppConfig.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/spring/SonarLintSpringAppConfig.java index 52e5f17091..70f858138a 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/spring/SonarLintSpringAppConfig.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/spring/SonarLintSpringAppConfig.java @@ -80,6 +80,7 @@ import org.sonarsource.sonarlint.core.plugin.skipped.SkippedPluginsNotifierService; import org.sonarsource.sonarlint.core.plugin.skipped.SkippedPluginsRepository; import org.sonarsource.sonarlint.core.promotion.PromotionService; +import org.sonarsource.sonarlint.core.remediation.aicodefix.AiCodeFixService; import org.sonarsource.sonarlint.core.reporting.FindingReportingService; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; @@ -187,7 +188,8 @@ UserAnalysisPropertiesRepository.class, OpenFilesRepository.class, DogfoodEnvironmentDetectionService.class, - MonitoringService.class + MonitoringService.class, + AiCodeFixService.class }) public class SonarLintSpringAppConfig { diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/storage/StorageService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/storage/StorageService.java index fdd89edc4a..8fdb9d7932 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/storage/StorageService.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/storage/StorageService.java @@ -45,7 +45,7 @@ public ConnectionStorage connection(String connectionId) { } public SonarProjectStorage binding(Binding binding) { - return connection(binding.getConnectionId()).project(binding.getSonarProjectKey()); + return connection(binding.connectionId()).project(binding.sonarProjectKey()); } @EventListener diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/HotspotSynchronizationService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/HotspotSynchronizationService.java index 4f7ebb514d..3f961f2058 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/HotspotSynchronizationService.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/HotspotSynchronizationService.java @@ -69,8 +69,8 @@ private static Version getSonarServerVersion(ServerApi serverApi, ConnectionStor } public void fetchProjectHotspots(Binding binding, String activeBranch, SonarLintCancelMonitor cancelMonitor) { - connectionManager.withValidConnection(binding.getConnectionId(), serverApi -> - downloadAllServerHotspots(binding.getConnectionId(), serverApi, binding.getSonarProjectKey(), activeBranch, cancelMonitor)); + connectionManager.withValidConnection(binding.connectionId(), serverApi -> + downloadAllServerHotspots(binding.connectionId(), serverApi, binding.sonarProjectKey(), activeBranch, cancelMonitor)); } private void downloadAllServerHotspots(String connectionId, ServerApi serverApi, String projectKey, String branchName, SonarLintCancelMonitor cancelMonitor) { @@ -83,8 +83,8 @@ private void downloadAllServerHotspots(String connectionId, ServerApi serverApi, } public void fetchFileHotspots(Binding binding, String activeBranch, Path serverFilePath, SonarLintCancelMonitor cancelMonitor) { - connectionManager.withValidConnection(binding.getConnectionId(), serverApi -> - downloadAllServerHotspotsForFile(binding.getConnectionId(), serverApi, binding.getSonarProjectKey(), serverFilePath, activeBranch, cancelMonitor)); + connectionManager.withValidConnection(binding.connectionId(), serverApi -> + downloadAllServerHotspotsForFile(binding.connectionId(), serverApi, binding.sonarProjectKey(), serverFilePath, activeBranch, cancelMonitor)); } private void downloadAllServerHotspotsForFile(String connectionId, ServerApi serverApi, String projectKey, Path serverRelativeFilePath, String branchName, diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/IssueSynchronizationService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/IssueSynchronizationService.java index 19022289ba..6ea6a50660 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/IssueSynchronizationService.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/IssueSynchronizationService.java @@ -61,8 +61,8 @@ public void syncServerIssuesForProject(ServerApi serverApi, String connectionId, } public void fetchProjectIssues(Binding binding, String activeBranch, SonarLintCancelMonitor cancelMonitor) { - connectionManager.withValidConnection(binding.getConnectionId(), serverApi -> - downloadServerIssuesForProject(binding.getConnectionId(), serverApi, binding.getSonarProjectKey(), activeBranch, cancelMonitor)); + connectionManager.withValidConnection(binding.connectionId(), serverApi -> + downloadServerIssuesForProject(binding.connectionId(), serverApi, binding.sonarProjectKey(), activeBranch, cancelMonitor)); } private void downloadServerIssuesForProject(String connectionId, ServerApi serverApi, String projectKey, String branchName, SonarLintCancelMonitor cancelMonitor) { @@ -74,8 +74,8 @@ private void downloadServerIssuesForProject(String connectionId, ServerApi serve } public void fetchFileIssues(Binding binding, Path serverFileRelativePath, String activeBranch, SonarLintCancelMonitor cancelMonitor) { - connectionManager.withValidConnection(binding.getConnectionId(), serverApi -> - downloadServerIssuesForFile(binding.getConnectionId(), serverApi, binding.getSonarProjectKey(), serverFileRelativePath, activeBranch, cancelMonitor)); + connectionManager.withValidConnection(binding.connectionId(), serverApi -> + downloadServerIssuesForFile(binding.connectionId(), serverApi, binding.sonarProjectKey(), serverFileRelativePath, activeBranch, cancelMonitor)); } public void downloadServerIssuesForFile(String connectionId, ServerApi serverApi, String projectKey, Path serverFileRelativePath, String branchName, diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/SynchronizationService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/SynchronizationService.java index 0ea2c91dca..140e2b0280 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/SynchronizationService.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/SynchronizationService.java @@ -388,8 +388,8 @@ public void onSonarProjectBranchChanged(MatchedSonarProjectBranchChangedEvent ch if (ignoreBranchEventForScopes.contains(configurationScopeId)) { return; } - configurationRepository.getEffectiveBinding(configurationScopeId).ifPresent(binding -> synchronizeProjectsAsync(Map.of(requireNonNull(binding.getConnectionId()), - Map.of(binding.getSonarProjectKey(), List.of(new BoundScope(configurationScopeId, binding.getConnectionId(), binding.getSonarProjectKey())))))); + configurationRepository.getEffectiveBinding(configurationScopeId).ifPresent(binding -> synchronizeProjectsAsync(Map.of(requireNonNull(binding.connectionId()), + Map.of(binding.sonarProjectKey(), List.of(new BoundScope(configurationScopeId, binding.connectionId(), binding.sonarProjectKey())))))); } @PreDestroy diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/TaintVulnerabilityTrackingService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/TaintVulnerabilityTrackingService.java index 3bec71b56d..aeee36b293 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/TaintVulnerabilityTrackingService.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/TaintVulnerabilityTrackingService.java @@ -234,11 +234,11 @@ private List loadTaintVulnerabilities(String configuratio return branchTrackingService.awaitEffectiveSonarProjectBranch(configurationScopeId) .map(matchedBranch -> { if (shouldRefresh) { - taintSynchronizationService.synchronizeTaintVulnerabilities(binding.getConnectionId(), binding.getSonarProjectKey(), cancelMonitor); + taintSynchronizationService.synchronizeTaintVulnerabilities(binding.connectionId(), binding.sonarProjectKey(), cancelMonitor); } var projectStorage = storageService.binding(binding); var newCodeDefinition = projectStorage.newCodeDefinition().read().>map(definition -> definition::isOnNewCode).orElse(date -> true); - var isMQRMode = severityModeService.isMQRModeForConnection(binding.getConnectionId()); + var isMQRMode = severityModeService.isMQRModeForConnection(binding.connectionId()); var pathTranslationOpt = pathTranslationService.getOrComputePathTranslation(configurationScopeId); return pathTranslationOpt.map(translation -> projectStorage.findings().loadTaint(matchedBranch) .stream().map(serverTaintIssue -> toDto(serverTaintIssue, newCodeDefinition, translation, isMQRMode)).toList()).orElse(null); diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/websocket/WebSocketService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/websocket/WebSocketService.java index adead130d7..8798c0d655 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/websocket/WebSocketService.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/websocket/WebSocketService.java @@ -243,7 +243,7 @@ private void considerAllBoundConfigurationScopes(Set configScopeIds) { private void considerScope(String scopeId) { var binding = getCurrentBinding(scopeId); - if (binding != null && isEligibleConnection(binding.getConnectionId())) { + if (binding != null && isEligibleConnection(binding.connectionId())) { subscribe(scopeId, binding); } else if (isSubscribedWithProjectKeyDifferentThanCurrentBinding(scopeId)) { forget(scopeId); @@ -264,8 +264,8 @@ private void removeProjectsFromSubscriptionListForConnection(String updatedConne } private void subscribe(String configScopeId, Binding binding) { - createConnectionIfNeeded(binding.getConnectionId()); - var projectKey = binding.getSonarProjectKey(); + createConnectionIfNeeded(binding.connectionId()); + var projectKey = binding.sonarProjectKey(); if (subscribedProjectKeysByConfigScopes.containsKey(configScopeId) && !subscribedProjectKeysByConfigScopes.get(configScopeId).equals(projectKey)) { forget(configScopeId); } diff --git a/backend/core/src/test/java/org/sonarsource/sonarlint/core/BindingClueProviderTests.java b/backend/core/src/test/java/org/sonarsource/sonarlint/core/BindingClueProviderTests.java index 7760c0fdca..ff864476f2 100644 --- a/backend/core/src/test/java/org/sonarsource/sonarlint/core/BindingClueProviderTests.java +++ b/backend/core/src/test/java/org/sonarsource/sonarlint/core/BindingClueProviderTests.java @@ -105,8 +105,8 @@ void should_detect_sonar_scanner_for_sonarcloud_based_on_url() { mockFindFileByNamesInScope( List.of(buildClientFile("sonar-project.properties", "path/to/sonar-project.properties", "sonar.host.url=https://sonarcloud.io\nsonar.projectKey=" + PROJECT_KEY_1))); - when(connectionRepository.getConnectionById(SC_CONNECTION_ID_1)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SC_CONNECTION_ID_1, MY_ORG_1, SonarCloudRegion.EU, true)); - when(connectionRepository.getConnectionById(SC_CONNECTION_ID_2)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SC_CONNECTION_ID_2, MY_ORG_2, SonarCloudRegion.EU, true)); + when(connectionRepository.getConnectionById(SC_CONNECTION_ID_1)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), SC_CONNECTION_ID_1, MY_ORG_1, SonarCloudRegion.EU, true)); + when(connectionRepository.getConnectionById(SC_CONNECTION_ID_2)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), SC_CONNECTION_ID_2, MY_ORG_2, SonarCloudRegion.EU, true)); var bindingClueWithConnections = underTest.collectBindingCluesWithConnections(CONFIG_SCOPE_ID, Set.of(SC_CONNECTION_ID_1, SC_CONNECTION_ID_2), new SonarLintCancelMonitor()); @@ -121,8 +121,8 @@ void should_detect_sonar_scanner_for_sonarcloud_based_on_url() { void should_detect_sonar_scanner_for_sonarcloud_based_on_organization() { mockFindFileByNamesInScope(List.of(buildClientFile("sonar-project.properties", "path/to/sonar-project.properties", "sonar.organization=" + MY_ORG_2))); - when(connectionRepository.getConnectionById(SC_CONNECTION_ID_1)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SC_CONNECTION_ID_1, MY_ORG_1, SonarCloudRegion.EU, true)); - when(connectionRepository.getConnectionById(SC_CONNECTION_ID_2)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SC_CONNECTION_ID_2, MY_ORG_2, SonarCloudRegion.EU, true)); + when(connectionRepository.getConnectionById(SC_CONNECTION_ID_1)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), SC_CONNECTION_ID_1, MY_ORG_1, SonarCloudRegion.EU, true)); + when(connectionRepository.getConnectionById(SC_CONNECTION_ID_2)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), SC_CONNECTION_ID_2, MY_ORG_2, SonarCloudRegion.EU, true)); var bindingClueWithConnections = underTest.collectBindingCluesWithConnections(CONFIG_SCOPE_ID, Set.of(SC_CONNECTION_ID_1, SC_CONNECTION_ID_2), new SonarLintCancelMonitor()); @@ -137,7 +137,7 @@ void should_detect_sonar_scanner_for_sonarcloud_based_on_organization() { void should_detect_autoscan_for_sonarcloud() { mockFindFileByNamesInScope(List.of(buildClientFile(".sonarcloud.properties", "path/to/.sonarcloud.properties", "sonar.projectKey=" + PROJECT_KEY_1))); - when(connectionRepository.getConnectionById(SC_CONNECTION_ID_1)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SC_CONNECTION_ID_1, MY_ORG_1, SonarCloudRegion.EU, true)); + when(connectionRepository.getConnectionById(SC_CONNECTION_ID_1)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), SC_CONNECTION_ID_1, MY_ORG_1, SonarCloudRegion.EU, true)); when(connectionRepository.getConnectionById(SQ_CONNECTION_ID_1)).thenReturn(new SonarQubeConnectionConfiguration(SQ_CONNECTION_ID_1, "http://mysonarqube.org", true)); var bindingClueWithConnections = underTest.collectBindingCluesWithConnections(CONFIG_SCOPE_ID, Set.of(SC_CONNECTION_ID_1, SQ_CONNECTION_ID_1), new SonarLintCancelMonitor()); @@ -153,7 +153,7 @@ void should_detect_autoscan_for_sonarcloud() { void should_detect_unknown_with_project_key() { mockFindFileByNamesInScope(List.of(buildClientFile("sonar-project.properties", "path/to/sonar-project.properties", "sonar.projectKey=" + PROJECT_KEY_1))); - when(connectionRepository.getConnectionById(SC_CONNECTION_ID_1)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SC_CONNECTION_ID_1, MY_ORG_1, SonarCloudRegion.EU, true)); + when(connectionRepository.getConnectionById(SC_CONNECTION_ID_1)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), SC_CONNECTION_ID_1, MY_ORG_1, SonarCloudRegion.EU, true)); when(connectionRepository.getConnectionById(SQ_CONNECTION_ID_1)).thenReturn(new SonarQubeConnectionConfiguration(SQ_CONNECTION_ID_1, "http://mysonarqube.org", true)); var bindingClueWithConnections = underTest.collectBindingCluesWithConnections(CONFIG_SCOPE_ID, Set.of(SC_CONNECTION_ID_1, SQ_CONNECTION_ID_1), new SonarLintCancelMonitor()); @@ -169,7 +169,7 @@ void should_detect_unknown_with_project_key() { void ignore_scanner_file_without_clue() { mockFindFileByNamesInScope(List.of(buildClientFile("sonar-project.properties", "path/to/sonar-project.properties", "sonar.sources=src"))); - when(connectionRepository.getConnectionById(SC_CONNECTION_ID_1)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SC_CONNECTION_ID_1, MY_ORG_1, SonarCloudRegion.EU, true)); + when(connectionRepository.getConnectionById(SC_CONNECTION_ID_1)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), SC_CONNECTION_ID_1, MY_ORG_1, SonarCloudRegion.EU, true)); when(connectionRepository.getConnectionById(SQ_CONNECTION_ID_1)).thenReturn(new SonarQubeConnectionConfiguration(SQ_CONNECTION_ID_1, "http://mysonarqube.org", true)); var bindingClueWithConnections = underTest.collectBindingCluesWithConnections(CONFIG_SCOPE_ID, Set.of(SC_CONNECTION_ID_1, SQ_CONNECTION_ID_1), new SonarLintCancelMonitor()); @@ -181,7 +181,7 @@ void ignore_scanner_file_without_clue() { void ignore_scanner_file_invalid_content() { mockFindFileByNamesInScope(List.of(buildClientFile("sonar-project.properties", "path/to/sonar-project.properties", "\\usonar.projectKey=" + PROJECT_KEY_1))); - when(connectionRepository.getConnectionById(SC_CONNECTION_ID_1)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SC_CONNECTION_ID_1, MY_ORG_1, SonarCloudRegion.EU, true)); + when(connectionRepository.getConnectionById(SC_CONNECTION_ID_1)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), SC_CONNECTION_ID_1, MY_ORG_1, SonarCloudRegion.EU, true)); when(connectionRepository.getConnectionById(SQ_CONNECTION_ID_1)).thenReturn(new SonarQubeConnectionConfiguration(SQ_CONNECTION_ID_1, "http://mysonarqube.org", true)); var bindingClueWithConnections = underTest.collectBindingCluesWithConnections(CONFIG_SCOPE_ID, Set.of(SC_CONNECTION_ID_1, SQ_CONNECTION_ID_1), new SonarLintCancelMonitor()); diff --git a/backend/core/src/test/java/org/sonarsource/sonarlint/core/BindingSuggestionProviderTests.java b/backend/core/src/test/java/org/sonarsource/sonarlint/core/BindingSuggestionProviderTests.java index e2e0fdf187..eff534b9ca 100644 --- a/backend/core/src/test/java/org/sonarsource/sonarlint/core/BindingSuggestionProviderTests.java +++ b/backend/core/src/test/java/org/sonarsource/sonarlint/core/BindingSuggestionProviderTests.java @@ -64,7 +64,7 @@ class BindingSuggestionProviderTests { public static final String SC_1_ID = "sc1"; public static final String SQ_2_ID = "sq2"; public static final SonarQubeConnectionConfiguration SQ_1 = new SonarQubeConnectionConfiguration(SQ_1_ID, "http://mysonarqube.com", true); - public static final SonarCloudConnectionConfiguration SC_1 = new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SC_1_ID, "myorg", SonarCloudRegion.EU, true); + public static final SonarCloudConnectionConfiguration SC_1 = new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), SC_1_ID, "myorg", SonarCloudRegion.EU, true); public static final String CONFIG_SCOPE_ID_1 = "configScope1"; public static final String PROJECT_KEY_1 = "projectKey1"; public static final ServerProject SERVER_PROJECT_1 = serverProject(PROJECT_KEY_1, "Project 1"); diff --git a/backend/core/src/test/java/org/sonarsource/sonarlint/core/ConnectionManagerTests.java b/backend/core/src/test/java/org/sonarsource/sonarlint/core/ConnectionManagerTests.java index 8c5e0ea4d2..9bf6346166 100644 --- a/backend/core/src/test/java/org/sonarsource/sonarlint/core/ConnectionManagerTests.java +++ b/backend/core/src/test/java/org/sonarsource/sonarlint/core/ConnectionManagerTests.java @@ -134,7 +134,7 @@ void getServerApi_returns_empty_if_connection_doesnt_exists() { @Test void getServerApi_returns_empty_if_client_cant_provide_httpclient() { - when(connectionRepository.getConnectionById("sc1")).thenReturn(new SonarCloudConnectionConfiguration(URI.create("http://server1"), "sc1", "myorg", SonarCloudRegion.EU, true)); + when(connectionRepository.getConnectionById("sc1")).thenReturn(new SonarCloudConnectionConfiguration(URI.create("http://server1"), URI.create("http://server1"), "sc1", "myorg", SonarCloudRegion.EU, true)); when(awareHttpClientProvider.getHttpClient("sc1", true)).thenReturn(null); var serverApi = underTest.getServerApi("sc1"); diff --git a/backend/core/src/test/java/org/sonarsource/sonarlint/core/TelemetryServerAttributesProviderTests.java b/backend/core/src/test/java/org/sonarsource/sonarlint/core/TelemetryServerAttributesProviderTests.java index 186565fc44..9cead7e98d 100644 --- a/backend/core/src/test/java/org/sonarsource/sonarlint/core/TelemetryServerAttributesProviderTests.java +++ b/backend/core/src/test/java/org/sonarsource/sonarlint/core/TelemetryServerAttributesProviderTests.java @@ -53,7 +53,7 @@ void it_should_calculate_connectedMode_usesSC_notDisabledNotifications_telemetry when(configurationRepository.getAllBoundScopes()).thenReturn(Set.of(new BoundScope(configurationScopeId, connectionId, projectKey))); var connectionConfigurationRepository = mock(ConnectionConfigurationRepository.class); - when(connectionConfigurationRepository.getConnectionById(connectionId)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), connectionId, "myTestOrg", SonarCloudRegion.EU, false)); + when(connectionConfigurationRepository.getConnectionById(connectionId)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), connectionId, "myTestOrg", SonarCloudRegion.EU, false)); var underTest = new TelemetryServerAttributesProvider(configurationRepository, connectionConfigurationRepository, mock(RulesService.class), mock(RulesRepository.class), mock(NodeJsService.class)); var telemetryLiveAttributes = underTest.getTelemetryServerLiveAttributes(); diff --git a/backend/core/src/test/java/org/sonarsource/sonarlint/core/VersionSoonUnsupportedHelperTests.java b/backend/core/src/test/java/org/sonarsource/sonarlint/core/VersionSoonUnsupportedHelperTests.java index c8eaed8d8d..9bc31086f5 100644 --- a/backend/core/src/test/java/org/sonarsource/sonarlint/core/VersionSoonUnsupportedHelperTests.java +++ b/backend/core/src/test/java/org/sonarsource/sonarlint/core/VersionSoonUnsupportedHelperTests.java @@ -63,7 +63,7 @@ class VersionSoonUnsupportedHelperTests { private static final String SC_CONNECTION_ID = "scConnectionId"; private static final SonarQubeConnectionConfiguration SQ_CONNECTION = new SonarQubeConnectionConfiguration(SQ_CONNECTION_ID, "https://mysonarqube.com", true); private static final SonarQubeConnectionConfiguration SQ_CONNECTION_2 = new SonarQubeConnectionConfiguration(SQ_CONNECTION_ID_2, "https://mysonarqube2.com", true); - private static final SonarCloudConnectionConfiguration SC_CONNECTION = new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SC_CONNECTION_ID, "https://sonarcloud.com", SonarCloudRegion.EU, true); + private static final SonarCloudConnectionConfiguration SC_CONNECTION = new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), SC_CONNECTION_ID, "https://sonarcloud.com", SonarCloudRegion.EU, true); private final SonarLintRpcClient client = mock(SonarLintRpcClient.class); private final ConnectionManager connectionManager = mock(ConnectionManager.class); diff --git a/backend/core/src/test/java/org/sonarsource/sonarlint/core/embedded/server/ShowFixSuggestionRequestHandlerTests.java b/backend/core/src/test/java/org/sonarsource/sonarlint/core/embedded/server/ShowFixSuggestionRequestHandlerTests.java index ac05e2c7b5..1ab9b61268 100644 --- a/backend/core/src/test/java/org/sonarsource/sonarlint/core/embedded/server/ShowFixSuggestionRequestHandlerTests.java +++ b/backend/core/src/test/java/org/sonarsource/sonarlint/core/embedded/server/ShowFixSuggestionRequestHandlerTests.java @@ -273,7 +273,7 @@ void should_cancel_flow_when_branch_does_not_match() throws HttpException, IOExc var context = mock(HttpContext.class); when(connectionConfigurationRepository.findByOrganization(any())).thenReturn(List.of( - new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), "name", "organizationKey", SonarCloudRegion.EU, false))); + new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), "name", "organizationKey", SonarCloudRegion.EU, false))); when(configurationRepository.getBoundScopesToConnectionAndSonarProject(any(), any())).thenReturn(List.of(new BoundScope("configScope", "connectionId", "projectKey"))); when(sonarLintRpcClient.matchProjectBranch(any())).thenReturn(CompletableFuture.completedFuture(new MatchProjectBranchResponse(false))); @@ -318,7 +318,7 @@ void should_find_main_branch_when_not_provided_and_not_stored() throws HttpExcep when(clientFile.getUri()).thenReturn(URI.create("file:///src/main/java/Main.java")); when(filePathTranslation.serverToIdePath(any())).thenReturn(Path.of("src/main/java/Main.java")); when(connectionConfigurationRepository.findByOrganization(any())).thenReturn(List.of( - new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), "name", "organizationKey", SonarCloudRegion.EU, false))); + new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), "name", "organizationKey", SonarCloudRegion.EU, false))); when(configurationRepository.getBoundScopesToConnectionAndSonarProject(any(), any())).thenReturn(List.of(new BoundScope("configScope", "connectionId", "projectKey"))); when(sonarLintRpcClient.matchProjectBranch(any())).thenReturn(CompletableFuture.completedFuture(new MatchProjectBranchResponse(true))); diff --git a/backend/core/src/test/java/org/sonarsource/sonarlint/core/embedded/server/ShowIssueRequestHandlerTests.java b/backend/core/src/test/java/org/sonarsource/sonarlint/core/embedded/server/ShowIssueRequestHandlerTests.java index 2d84e7ef5a..e6d364d0ad 100644 --- a/backend/core/src/test/java/org/sonarsource/sonarlint/core/embedded/server/ShowIssueRequestHandlerTests.java +++ b/backend/core/src/test/java/org/sonarsource/sonarlint/core/embedded/server/ShowIssueRequestHandlerTests.java @@ -366,7 +366,7 @@ void should_cancel_flow_when_branch_does_not_match() throws HttpException, IOExc var context = mock(HttpContext.class); when(connectionConfigurationRepository.findByOrganization(any())).thenReturn(List.of( - new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), "name", "organizationKey", SonarCloudRegion.EU, false))); + new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), "name", "organizationKey", SonarCloudRegion.EU, false))); when(configurationRepository.getBoundScopesToConnectionAndSonarProject(any(), any())).thenReturn(List.of(new BoundScope("configScope" , "connectionId", "projectKey"))); when(sonarLintRpcClient.matchProjectBranch(any())).thenReturn(CompletableFuture.completedFuture(new MatchProjectBranchResponse(false))); @@ -395,7 +395,7 @@ void should_find_main_branch_when_branch_is_not_provided() throws HttpException, var context = mock(HttpContext.class); when(connectionConfigurationRepository.findByOrganization(any())).thenReturn(List.of( - new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), "name", "organizationKey", SonarCloudRegion.EU, false))); + new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), "name", "organizationKey", SonarCloudRegion.EU, false))); when(configurationRepository.getBoundScopesToConnectionAndSonarProject(any(), any())).thenReturn(List.of(new BoundScope("configScope" , "connectionId", "projectKey"))); when(sonarLintRpcClient.matchProjectBranch(any())).thenReturn(CompletableFuture.completedFuture(new MatchProjectBranchResponse(true))); @@ -425,7 +425,7 @@ void should_find_main_branch_when_not_provided_and_not_stored() throws HttpExcep var context = mock(HttpContext.class); when(connectionConfigurationRepository.findByOrganization(any())).thenReturn(List.of( - new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), "name", "organizationKey", SonarCloudRegion.EU, false))); + new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), "name", "organizationKey", SonarCloudRegion.EU, false))); when(configurationRepository.getBoundScopesToConnectionAndSonarProject(any(), any())).thenReturn(List.of(new BoundScope("configScope" , "connectionId", "projectKey"))); when(sonarLintRpcClient.matchProjectBranch(any())).thenReturn(CompletableFuture.completedFuture(new MatchProjectBranchResponse(true))); diff --git a/backend/core/src/test/java/org/sonarsource/sonarlint/core/hotspot/HotspotServiceTests.java b/backend/core/src/test/java/org/sonarsource/sonarlint/core/hotspot/HotspotServiceTests.java index a09145b854..2a8996ad56 100644 --- a/backend/core/src/test/java/org/sonarsource/sonarlint/core/hotspot/HotspotServiceTests.java +++ b/backend/core/src/test/java/org/sonarsource/sonarlint/core/hotspot/HotspotServiceTests.java @@ -28,13 +28,13 @@ class HotspotServiceTests { @Test void testBuildSonarQubeHotspotUrl() { - assertThat(HotspotService.buildHotspotUrl("myProject", "myBranch", "hotspotKey", new EndpointParams("http://foo.com", false, null))) + assertThat(HotspotService.buildHotspotUrl("myProject", "myBranch", "hotspotKey", new EndpointParams("http://foo.com", "", false, null))) .isEqualTo("http://foo.com/security_hotspots?id=myProject&branch=myBranch&hotspots=hotspotKey"); } @Test void testBuildSonarCloudHotspotUrl() { - assertThat(HotspotService.buildHotspotUrl("myProject", "myBranch", "hotspotKey", new EndpointParams("https://sonarcloud.io", true, "myOrg"))) + assertThat(HotspotService.buildHotspotUrl("myProject", "myBranch", "hotspotKey", new EndpointParams("https://sonarcloud.io", "", true, "myOrg"))) .isEqualTo("https://sonarcloud.io/project/security_hotspots?id=myProject&branch=myBranch&hotspots=hotspotKey"); } -} \ No newline at end of file +} diff --git a/backend/core/src/test/java/org/sonarsource/sonarlint/core/repository/config/ConfigurationRepositoryTest.java b/backend/core/src/test/java/org/sonarsource/sonarlint/core/repository/config/ConfigurationRepositoryTest.java index b11fb6395c..71c1c3de87 100644 --- a/backend/core/src/test/java/org/sonarsource/sonarlint/core/repository/config/ConfigurationRepositoryTest.java +++ b/backend/core/src/test/java/org/sonarsource/sonarlint/core/repository/config/ConfigurationRepositoryTest.java @@ -57,8 +57,8 @@ void it_should_consider_the_binding_configured_on_a_scope_as_effective() { assertThat(binding) .hasValueSatisfying(b -> { - assertThat(b.getConnectionId()).isEqualTo("connectionId"); - assertThat(b.getSonarProjectKey()).isEqualTo("projectKey"); + assertThat(b.connectionId()).isEqualTo("connectionId"); + assertThat(b.sonarProjectKey()).isEqualTo("projectKey"); }); } @@ -71,8 +71,8 @@ void it_should_get_the_effective_binding_from_parent_if_child_is_unbound() { assertThat(binding) .hasValueSatisfying(b -> { - assertThat(b.getConnectionId()).isEqualTo("connectionId"); - assertThat(b.getSonarProjectKey()).isEqualTo("projectKey"); + assertThat(b.connectionId()).isEqualTo("connectionId"); + assertThat(b.sonarProjectKey()).isEqualTo("projectKey"); }); } diff --git a/backend/core/src/test/java/org/sonarsource/sonarlint/core/repository/connection/SonarCloudConnectionConfigurationTest.java b/backend/core/src/test/java/org/sonarsource/sonarlint/core/repository/connection/SonarCloudConnectionConfigurationTest.java index 9a994a882f..dcbe8dcc81 100644 --- a/backend/core/src/test/java/org/sonarsource/sonarlint/core/repository/connection/SonarCloudConnectionConfigurationTest.java +++ b/backend/core/src/test/java/org/sonarsource/sonarlint/core/repository/connection/SonarCloudConnectionConfigurationTest.java @@ -28,13 +28,13 @@ class SonarCloudConnectionConfigurationTest { @Test void testEqualsAndHashCode() { - var underTest = new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), "id1", "org1", SonarCloudRegion.EU, true); + var underTest = new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), "id1", "org1", SonarCloudRegion.EU, true); assertThat(underTest) - .isEqualTo(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), "id1", "org1", SonarCloudRegion.EU, true)) - .isNotEqualTo(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), "id2", "org1", SonarCloudRegion.EU, true)) - .isNotEqualTo(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), "id1", "org2", SonarCloudRegion.EU, true)) + .isEqualTo(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), "id1", "org1", SonarCloudRegion.EU, true)) + .isNotEqualTo(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), "id2", "org1", SonarCloudRegion.EU, true)) + .isNotEqualTo(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), "id1", "org2", SonarCloudRegion.EU, true)) .isNotEqualTo(new SonarQubeConnectionConfiguration("id1", "http://server1", true)) - .hasSameHashCodeAs(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), "id1", "org1", SonarCloudRegion.EU, true)); + .hasSameHashCodeAs(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), "id1", "org1", SonarCloudRegion.EU, true)); } } diff --git a/backend/core/src/test/java/org/sonarsource/sonarlint/core/repository/connection/SonarQubeConnectionConfigurationTest.java b/backend/core/src/test/java/org/sonarsource/sonarlint/core/repository/connection/SonarQubeConnectionConfigurationTest.java index 290fe19dc8..1ec5740924 100644 --- a/backend/core/src/test/java/org/sonarsource/sonarlint/core/repository/connection/SonarQubeConnectionConfigurationTest.java +++ b/backend/core/src/test/java/org/sonarsource/sonarlint/core/repository/connection/SonarQubeConnectionConfigurationTest.java @@ -51,7 +51,7 @@ void testEqualsAndHashCode() { .isEqualTo(new SonarQubeConnectionConfiguration("id1", "http://server1", true)) .isNotEqualTo(new SonarQubeConnectionConfiguration("id2", "http://server1", true)) .isNotEqualTo(new SonarQubeConnectionConfiguration("id1", "http://server2", true)) - .isNotEqualTo(new SonarCloudConnectionConfiguration(URI.create("http://server1"), "id1", "org1", SonarCloudRegion.EU, true)) + .isNotEqualTo(new SonarCloudConnectionConfiguration(URI.create("http://server1"), URI.create("http://server1"), "id1", "org1", SonarCloudRegion.EU, true)) .hasSameHashCodeAs(new SonarQubeConnectionConfiguration("id1", "http://server1", true)); } diff --git a/backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/AiCodeFixRpcServiceDelegate.java b/backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/AiCodeFixRpcServiceDelegate.java new file mode 100644 index 0000000000..c2cb88ec83 --- /dev/null +++ b/backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/AiCodeFixRpcServiceDelegate.java @@ -0,0 +1,38 @@ +/* + * SonarLint Core - RPC Implementation + * 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. + */ +package org.sonarsource.sonarlint.core.rpc.impl; + +import java.util.concurrent.CompletableFuture; +import org.sonarsource.sonarlint.core.remediation.aicodefix.AiCodeFixService; +import org.sonarsource.sonarlint.core.rpc.protocol.backend.remediation.aicodefix.AiCodeFixRpcService; +import org.sonarsource.sonarlint.core.rpc.protocol.backend.remediation.aicodefix.SuggestFixParams; +import org.sonarsource.sonarlint.core.rpc.protocol.backend.remediation.aicodefix.SuggestFixResponse; + +public class AiCodeFixRpcServiceDelegate extends AbstractRpcServiceDelegate implements AiCodeFixRpcService { + + public AiCodeFixRpcServiceDelegate(SonarLintRpcServerImpl sonarLintRpcServer) { + super(sonarLintRpcServer); + } + + @Override + public CompletableFuture suggestFix(SuggestFixParams params) { + return requestAsync(cancelMonitor -> getBean(AiCodeFixService.class).suggestFix(params.getConfigurationScopeId(), params.getIssueId(), cancelMonitor)); + } +} diff --git a/backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/SonarLintRpcServerImpl.java b/backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/SonarLintRpcServerImpl.java index 46bcf670d3..23c6046528 100644 --- a/backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/SonarLintRpcServerImpl.java +++ b/backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/SonarLintRpcServerImpl.java @@ -61,6 +61,7 @@ import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.IssueRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.newcode.NewCodeRpcService; +import org.sonarsource.sonarlint.core.rpc.protocol.backend.remediation.aicodefix.AiCodeFixRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.RulesRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.telemetry.TelemetryRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.TaintVulnerabilityTrackingRpcService; @@ -230,6 +231,11 @@ public DogfoodingRpcService getDogfoodingService() { return new DogfoodingRpcServiceDelegate(this); } + @Override + public AiCodeFixRpcService getAiCodeFixRpcService() { + return new AiCodeFixRpcServiceDelegate(this); + } + @Override public CompletableFuture shutdown() { LOG.info("SonarLint backend shutting down, instance={}", this); diff --git a/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/EndpointParams.java b/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/EndpointParams.java index 067467047b..b484b9d596 100644 --- a/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/EndpointParams.java +++ b/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/EndpointParams.java @@ -20,6 +20,7 @@ package org.sonarsource.sonarlint.core.serverapi; import java.util.Optional; +import javax.annotation.CheckForNull; import javax.annotation.Nullable; /** @@ -28,12 +29,16 @@ public class EndpointParams { private final String baseUrl; + @Nullable + // For SonarQube Cloud, some APIs are located under a dedicated subdomain. Null for SonarQube Server + private final String apiBaseUrl; private final boolean sonarCloud; @Nullable private final String organization; - public EndpointParams(String baseUrl, boolean isSonarCloud, @Nullable String organization) { + public EndpointParams(String baseUrl, @Nullable String apiBaseUrl, boolean isSonarCloud, @Nullable String organization) { this.baseUrl = baseUrl; + this.apiBaseUrl = apiBaseUrl; this.sonarCloud = isSonarCloud; this.organization = organization; } @@ -42,6 +47,11 @@ public String getBaseUrl() { return baseUrl; } + @CheckForNull + public String getApiBaseUrl() { + return apiBaseUrl; + } + public boolean isSonarCloud() { return sonarCloud; } diff --git a/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/ServerApi.java b/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/ServerApi.java index 9dfa9cc574..dab13639ff 100644 --- a/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/ServerApi.java +++ b/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/ServerApi.java @@ -24,6 +24,7 @@ import org.sonarsource.sonarlint.core.serverapi.branches.ProjectBranchesApi; import org.sonarsource.sonarlint.core.serverapi.component.ComponentApi; import org.sonarsource.sonarlint.core.serverapi.developers.DevelopersApi; +import org.sonarsource.sonarlint.core.serverapi.fixsuggestions.FixSuggestionsApi; import org.sonarsource.sonarlint.core.serverapi.hotspot.HotspotApi; import org.sonarsource.sonarlint.core.serverapi.issue.IssueApi; import org.sonarsource.sonarlint.core.serverapi.newcode.NewCodeApi; @@ -112,6 +113,10 @@ public NewCodeApi newCodeApi() { return new NewCodeApi(helper); } + public FixSuggestionsApi fixSuggestions() { + return new FixSuggestionsApi(helper); + } + public boolean isSonarCloud() { return helper.isSonarCloud(); } diff --git a/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/ServerApiHelper.java b/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/ServerApiHelper.java index 53f89d4e9d..300fed7264 100644 --- a/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/ServerApiHelper.java +++ b/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/ServerApiHelper.java @@ -48,6 +48,8 @@ import org.sonarsource.sonarlint.core.serverapi.exception.ServerErrorException; import org.sonarsource.sonarlint.core.serverapi.exception.UnauthorizedException; +import static java.util.Objects.requireNonNull; + /** * Wrapper around HttpClient to avoid repetitive code, like support of pagination, and log timing of requests */ @@ -78,7 +80,15 @@ public HttpClient.Response get(String path, SonarLintCancelMonitor cancelMonitor return response; } - public HttpClient.Response post(String url, String contentType, String body, SonarLintCancelMonitor cancelMonitor) { + public HttpClient.Response post(String relativePath, String contentType, String body, SonarLintCancelMonitor cancelMonitor) { + return postUrl(buildEndpointUrl(relativePath), contentType, body, cancelMonitor); + } + + public HttpClient.Response apiPost(String relativePath, String contentType, String body, SonarLintCancelMonitor cancelMonitor) { + return postUrl(buildApiEndpointUrl(relativePath), contentType, body, cancelMonitor); + } + + private HttpClient.Response postUrl(String url, String contentType, String body, SonarLintCancelMonitor cancelMonitor) { var response = rawPost(url, contentType, body, cancelMonitor); if (!response.isSuccessful()) { throw handleError(response); @@ -97,10 +107,8 @@ public HttpClient.Response rawGet(String relativePath, SonarLintCancelMonitor ca return processResponse("GET", cancelMonitor, httpFuture, startTime, url); } - public HttpClient.Response rawPost(String relativePath, String contentType, String body, SonarLintCancelMonitor cancelMonitor) { + public HttpClient.Response rawPost(String url, String contentType, String body, SonarLintCancelMonitor cancelMonitor) { var startTime = Instant.now(); - var url = buildEndpointUrl(relativePath); - var httpFuture = client.postAsync(url, contentType, body); return processResponse("POST", cancelMonitor, httpFuture, startTime, url); } @@ -132,6 +140,10 @@ private String buildEndpointUrl(String relativePath) { return concat(endpointParams.getBaseUrl(), relativePath); } + private String buildApiEndpointUrl(String relativePath) { + return concat(requireNonNull(endpointParams.getApiBaseUrl()), relativePath); + } + public static String concat(String baseUrl, String relativePath) { return StringUtils.appendIfMissing(baseUrl, "/") + (relativePath.startsWith("/") ? relativePath.substring(1) : relativePath); diff --git a/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/AiSuggestionRequestBodyDto.java b/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/AiSuggestionRequestBodyDto.java new file mode 100644 index 0000000000..7129448c49 --- /dev/null +++ b/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/AiSuggestionRequestBodyDto.java @@ -0,0 +1,25 @@ +/* + * SonarLint Core - Server API + * 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. + */ +package org.sonarsource.sonarlint.core.serverapi.fixsuggestions; + +public record AiSuggestionRequestBodyDto(String organizationKey, String projectKey, Issue issue) { + public record Issue(String message, Integer startLine, Integer endLine, String ruleKey, String sourceCode) { + } +} diff --git a/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/AiSuggestionResponseBodyDto.java b/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/AiSuggestionResponseBodyDto.java new file mode 100644 index 0000000000..6f9a3445b4 --- /dev/null +++ b/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/AiSuggestionResponseBodyDto.java @@ -0,0 +1,28 @@ +/* + * SonarLint Core - Server API + * 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. + */ +package org.sonarsource.sonarlint.core.serverapi.fixsuggestions; + +import java.util.List; +import java.util.UUID; + +public record AiSuggestionResponseBodyDto(UUID id, String explanation, List changes) { + public record ChangeDto(int startLine, int endLine, String newCode) { + } +} diff --git a/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/FixSuggestionsApi.java b/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/FixSuggestionsApi.java new file mode 100644 index 0000000000..912fb4bb0d --- /dev/null +++ b/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/FixSuggestionsApi.java @@ -0,0 +1,49 @@ +/* + * SonarLint Core - Server API + * 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. + */ +package org.sonarsource.sonarlint.core.serverapi.fixsuggestions; + +import com.google.gson.GsonBuilder; +import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; +import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; +import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; +import org.sonarsource.sonarlint.core.serverapi.exception.UnexpectedBodyException; + +import static org.sonarsource.sonarlint.core.http.HttpClient.JSON_CONTENT_TYPE; + +public class FixSuggestionsApi { + private static final SonarLintLogger LOG = SonarLintLogger.get(); + + private final ServerApiHelper helper; + + public FixSuggestionsApi(ServerApiHelper helper) { + this.helper = helper; + } + + public AiSuggestionResponseBodyDto getAiSuggestion(AiSuggestionRequestBodyDto dto, SonarLintCancelMonitor cancelMonitor) { + // avoid Gson replacing characters like < > or = with Unicode representation + var gson = new GsonBuilder().disableHtmlEscaping().create(); + try (var response = helper.apiPost("/fix-suggestions/ai-suggestions", JSON_CONTENT_TYPE, gson.toJson(dto), cancelMonitor)) { + return gson.fromJson(response.bodyAsString(), AiSuggestionResponseBodyDto.class); + } catch (Exception e) { + LOG.error("Error while generating an AI CodeFix", e); + throw new UnexpectedBodyException(e); + } + } +} diff --git a/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/package-info.java b/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/package-info.java new file mode 100644 index 0000000000..35034024ea --- /dev/null +++ b/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarLint Core - Server API + * 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. + */ +@ParametersAreNonnullByDefault +package org.sonarsource.sonarlint.core.serverapi.fixsuggestions; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/MockWebServerExtensionWithProtobuf.java b/backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/MockWebServerExtensionWithProtobuf.java index d9f3fac0a0..7e3e0f6543 100644 --- a/backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/MockWebServerExtensionWithProtobuf.java +++ b/backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/MockWebServerExtensionWithProtobuf.java @@ -77,7 +77,7 @@ public EndpointParams endpointParams() { } public EndpointParams endpointParams(@Nullable String organizationKey) { - return new EndpointParams(url("/"), organizationKey != null, organizationKey); + return new EndpointParams(url("/"), url("/"), organizationKey != null, organizationKey); } diff --git a/backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/FixSuggestionsApiTest.java b/backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/FixSuggestionsApiTest.java new file mode 100644 index 0000000000..d4c6f7fd8d --- /dev/null +++ b/backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/FixSuggestionsApiTest.java @@ -0,0 +1,87 @@ +/* + * SonarLint Core - Server API + * 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. + */ +package org.sonarsource.sonarlint.core.serverapi.fixsuggestions; + +import java.util.List; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; +import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; +import org.sonarsource.sonarlint.core.serverapi.MockWebServerExtensionWithProtobuf; +import org.sonarsource.sonarlint.core.serverapi.exception.UnexpectedBodyException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +class FixSuggestionsApiTest { + @RegisterExtension + private static final SonarLintLogTester logTester = new SonarLintLogTester(); + + @RegisterExtension + static MockWebServerExtensionWithProtobuf mockServer = new MockWebServerExtensionWithProtobuf(); + + private FixSuggestionsApi underTest; + + @BeforeEach + void setUp() { + underTest = new FixSuggestionsApi(mockServer.serverApiHelper()); + } + + @Test + void it_should_throw_an_exception_if_the_body_is_malformed() { + mockServer.addStringResponse("/fix-suggestions/ai-suggestions", """ + { + "id": "XXX + } + """); + + var throwable = catchThrowable(() -> underTest.getAiSuggestion( + new AiSuggestionRequestBodyDto("orgKey", "projectKey", new AiSuggestionRequestBodyDto.Issue("message", 0, 0, "rule:key", "source")), new SonarLintCancelMonitor())); + + assertThat(throwable).isInstanceOf(UnexpectedBodyException.class); + } + + @Test + void it_should_return_the_generated_suggestion() { + mockServer.addStringResponse("/fix-suggestions/ai-suggestions", """ + { + "id": "9d4e18f6-f79f-41ad-a480-1c96bd58d58f", + "explanation": "This is the way", + "changes": [ + { + "startLine": 0, + "endLine": 0, + "newCode": "This is the new code" + } + ] + } + """); + + var response = underTest.getAiSuggestion(new AiSuggestionRequestBodyDto("orgKey", "projectKey", new AiSuggestionRequestBodyDto.Issue("message", 0, 0, "rule:key", "source")), + new SonarLintCancelMonitor()); + + assertThat(response) + .isEqualTo(new AiSuggestionResponseBodyDto(UUID.fromString("9d4e18f6-f79f-41ad-a480-1c96bd58d58f"), "This is the way", + List.of(new AiSuggestionResponseBodyDto.ChangeDto(0, 0, "This is the new code")))); + } + +} diff --git a/backend/server-connection/src/test/java/testutils/MockWebServerExtensionWithProtobuf.java b/backend/server-connection/src/test/java/testutils/MockWebServerExtensionWithProtobuf.java index 4638ac0845..4392fd53cd 100644 --- a/backend/server-connection/src/test/java/testutils/MockWebServerExtensionWithProtobuf.java +++ b/backend/server-connection/src/test/java/testutils/MockWebServerExtensionWithProtobuf.java @@ -79,7 +79,7 @@ public EndpointParams endpointParams() { } public EndpointParams endpointParams(@Nullable String organizationKey) { - return new EndpointParams(url("/"), organizationKey != null, organizationKey); + return new EndpointParams(url("/"), url("/"), organizationKey != null, organizationKey); } } diff --git a/medium-tests/src/test/java/mediumtest/remediation/aicodefix/AiCodeFixMediumTest.java b/medium-tests/src/test/java/mediumtest/remediation/aicodefix/AiCodeFixMediumTest.java new file mode 100644 index 0000000000..815a6b2964 --- /dev/null +++ b/medium-tests/src/test/java/mediumtest/remediation/aicodefix/AiCodeFixMediumTest.java @@ -0,0 +1,258 @@ +/* + * SonarLint Core - Medium Tests + * 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. + */ +package mediumtest.remediation.aicodefix; + +import java.nio.file.Path; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.UUID; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; +import org.junit.jupiter.api.io.TempDir; +import org.sonarsource.sonarlint.core.rpc.protocol.backend.file.DidUpdateFileSystemParams; +import org.sonarsource.sonarlint.core.rpc.protocol.backend.remediation.aicodefix.SuggestFixChangeDto; +import org.sonarsource.sonarlint.core.rpc.protocol.backend.remediation.aicodefix.SuggestFixParams; +import org.sonarsource.sonarlint.core.rpc.protocol.common.ClientFileDto; +import org.sonarsource.sonarlint.core.test.utils.junit5.SonarLintTest; +import org.sonarsource.sonarlint.core.test.utils.junit5.SonarLintTestHarness; +import utils.TestPlugin; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode.InvalidParams; +import static org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode.CONFIG_SCOPE_NOT_BOUND; +import static org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode.CONNECTION_KIND_NOT_SUPPORTED; +import static org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode.CONNECTION_NOT_FOUND; +import static org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode.FILE_NOT_FOUND; +import static org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode.ISSUE_NOT_FOUND; +import static utils.AnalysisUtils.analyzeFileAndGetIssue; +import static utils.AnalysisUtils.createFile; + +public class AiCodeFixMediumTest { + + @SonarLintTest + void it_should_fail_if_the_configuration_scope_is_not_bound(SonarLintTestHarness harness) { + var backend = harness.newBackend() + .withUnboundConfigScope("configScope") + .start(); + + var future = backend.getAiCodeFixRpcService().suggestFix(new SuggestFixParams("configScope", UUID.randomUUID())); + + assertThat(future).failsWithin(Duration.of(1, ChronoUnit.SECONDS)) + .withThrowableThat() + .havingCause() + .isInstanceOf(ResponseErrorException.class) + .asInstanceOf(InstanceOfAssertFactories.type(ResponseErrorException.class)) + .extracting(ResponseErrorException::getResponseError) + .extracting(ResponseError::getCode, ResponseError::getMessage) + .containsExactly(CONFIG_SCOPE_NOT_BOUND, "The provided configuration scope is not bound"); + } + + @SonarLintTest + void it_should_fail_if_the_configuration_scope_is_bound_to_sonarqube(SonarLintTestHarness harness) { + var backend = harness.newBackend() + .withSonarQubeConnection("connectionId") + .withBoundConfigScope("configScope", "connectionId", "projectKey") + .start(); + + var future = backend.getAiCodeFixRpcService().suggestFix(new SuggestFixParams("configScope", UUID.randomUUID())); + + assertThat(future).failsWithin(Duration.of(1, ChronoUnit.SECONDS)) + .withThrowableThat() + .havingCause() + .isInstanceOf(ResponseErrorException.class) + .asInstanceOf(InstanceOfAssertFactories.type(ResponseErrorException.class)) + .extracting(ResponseErrorException::getResponseError) + .extracting(ResponseError::getCode, ResponseError::getMessage) + .containsExactly(CONNECTION_KIND_NOT_SUPPORTED, "The provided configuration scope is not bound to SonarQube Cloud"); + } + + @SonarLintTest + void it_should_fail_if_the_configuration_scope_is_bound_to_an_unknown_connection(SonarLintTestHarness harness) { + var backend = harness.newBackend() + .withBoundConfigScope("configScope", "connectionId", "projectKey") + .start(); + + var future = backend.getAiCodeFixRpcService().suggestFix(new SuggestFixParams("configScope", UUID.randomUUID())); + + assertThat(future).failsWithin(Duration.of(1, ChronoUnit.SECONDS)) + .withThrowableThat() + .havingCause() + .isInstanceOf(ResponseErrorException.class) + .asInstanceOf(InstanceOfAssertFactories.type(ResponseErrorException.class)) + .extracting(ResponseErrorException::getResponseError) + .extracting(ResponseError::getCode, ResponseError::getMessage) + .containsExactly(CONNECTION_NOT_FOUND, "The provided configuration scope is bound to an unknown connection"); + } + + @SonarLintTest + void it_should_fail_if_the_issue_is_unknown(SonarLintTestHarness harness) { + var backend = harness.newBackend() + .withSonarCloudConnection("connectionId", "organizationKey", true, storage -> storage + .withProject("projectKey")) + .withBoundConfigScope("configScope", "connectionId", "projectKey") + .start(); + var issueId = UUID.randomUUID(); + + var future = backend.getAiCodeFixRpcService().suggestFix(new SuggestFixParams("configScope", issueId)); + + assertThat(future).failsWithin(Duration.of(1, ChronoUnit.SECONDS)) + .withThrowableThat() + .havingCause() + .isInstanceOf(ResponseErrorException.class) + .asInstanceOf(InstanceOfAssertFactories.type(ResponseErrorException.class)) + .extracting(ResponseErrorException::getResponseError) + .extracting(ResponseError::getCode, ResponseError::getMessage) + .containsExactly(ISSUE_NOT_FOUND, "The provided issue does not exist"); + } + + @SonarLintTest + void it_should_fail_if_the_file_is_unknown(SonarLintTestHarness harness, @TempDir Path baseDir) throws InterruptedException { + var sourceCode = """ + + + 4.0.0 + com.foo + bar + ${pom.version} + """; + var filePath = createFile(baseDir, "pom.xml", sourceCode); + var fileUri = filePath.toUri(); + var server = harness.newFakeSonarCloudServer("organizationKey") + .withProject("projectKey", + project -> project.withBranch("branchName") + .withAiCodeFix(aiCodeFix -> aiCodeFix + .withId(UUID.fromString("e51b7bbd-72bc-4008-a4f1-d75583f3dc98")) + .withExplanation("This is the explanation") + .withChange(0, 0, "This is the new code"))) + .start(); + var fakeClient = harness.newFakeClient() + .withInitialFs("configScope", baseDir, List.of(new ClientFileDto(fileUri, baseDir.relativize(filePath), "configScope", false, null, filePath, null, null, true))) + .build(); + var backend = harness.newBackend() + .withConnectedEmbeddedPluginAndEnabledLanguage(TestPlugin.XML) + .withSonarCloudUrl(server.baseUrl()) + .withSonarCloudConnection("connectionId", "organizationKey", true, storage -> storage + .withProject("projectKey", project -> project.withRuleSet("xml", ruleSet -> ruleSet.withActiveRule("xml:S3421", "MAJOR")))) + .withBoundConfigScope("configScope", "connectionId", "projectKey") + .start(fakeClient); + var issue = analyzeFileAndGetIssue(fileUri, fakeClient, backend, "configScope"); + backend.getFileService().didUpdateFileSystem(new DidUpdateFileSystemParams(List.of(), List.of(), List.of(fileUri))); + // leave time for the notification to be received by the backend + Thread.sleep(300); + + var future = backend.getAiCodeFixRpcService().suggestFix(new SuggestFixParams("configScope", issue.getId())); + + assertThat(future).failsWithin(Duration.of(1, ChronoUnit.SECONDS)) + .withThrowableThat() + .havingCause() + .isInstanceOf(ResponseErrorException.class) + .asInstanceOf(InstanceOfAssertFactories.type(ResponseErrorException.class)) + .extracting(ResponseErrorException::getResponseError) + .extracting(ResponseError::getCode, ResponseError::getMessage) + .containsExactly(FILE_NOT_FOUND, "The provided issue ID corresponds to an unknown file"); + } + + @SonarLintTest + void it_should_fail_if_the_issue_is_not_fixable_because_at_file_level(SonarLintTestHarness harness, @TempDir Path baseDir) { + var sourceCode = "public interface Fubar\n" + + "{}"; + var filePath = createFile(baseDir, "Fubar.java", sourceCode); + var fileUri = filePath.toUri(); + var server = harness.newFakeSonarCloudServer("organizationKey") + .withProject("projectKey", + project -> project.withBranch("branchName") + .withAiCodeFix(aiCodeFix -> aiCodeFix + .withId(UUID.fromString("e51b7bbd-72bc-4008-a4f1-d75583f3dc98")) + .withExplanation("This is the explanation") + .withChange(0, 0, "This is the new code"))) + .start(); + var fakeClient = harness.newFakeClient() + .withInitialFs("configScope", baseDir, List.of(new ClientFileDto(fileUri, baseDir.relativize(filePath), "configScope", false, null, filePath, null, null, true))) + .build(); + var backend = harness.newBackend() + .withConnectedEmbeddedPluginAndEnabledLanguage(TestPlugin.JAVA) + .withSonarCloudUrl(server.baseUrl()) + .withSonarCloudConnection("connectionId", "organizationKey", true, storage -> storage + .withProject("projectKey", project -> project.withRuleSet("java", ruleSet -> ruleSet.withActiveRule("java:S1220", "MAJOR")))) + .withBoundConfigScope("configScope", "connectionId", "projectKey") + .start(fakeClient); + var issue = analyzeFileAndGetIssue(fileUri, fakeClient, backend, "configScope"); + + var future = backend.getAiCodeFixRpcService().suggestFix(new SuggestFixParams("configScope", issue.getId())); + + assertThat(future).failsWithin(Duration.of(1, ChronoUnit.SECONDS)) + .withThrowableThat() + .havingCause() + .isInstanceOf(ResponseErrorException.class) + .asInstanceOf(InstanceOfAssertFactories.type(ResponseErrorException.class)) + .extracting(ResponseErrorException::getResponseError) + .extracting(ResponseError::getCode, ResponseError::getMessage) + .containsExactly(InvalidParams.getValue(), "The provided issue cannot be fixed"); + } + + @SonarLintTest + void it_should_return_the_suggestion_from_sonarqube_cloud(SonarLintTestHarness harness, @TempDir Path baseDir) { + var sourceCode = """ + + + 4.0.0 + com.foo + bar + ${pom.version} + """; + var filePath = createFile(baseDir, "pom.xml", sourceCode); + var fileUri = filePath.toUri(); + var server = harness.newFakeSonarCloudServer("organizationKey") + .withProject("projectKey", + project -> project.withBranch("branchName") + .withAiCodeFix(aiCodeFix -> aiCodeFix + .withId(UUID.fromString("e51b7bbd-72bc-4008-a4f1-d75583f3dc98")) + .withExplanation("This is the explanation") + .withChange(0, 0, "This is the new code"))) + .start(); + var fakeClient = harness.newFakeClient() + .withInitialFs("configScope", baseDir, List.of(new ClientFileDto(fileUri, baseDir.relativize(filePath), "configScope", false, null, filePath, null, null, true))) + .build(); + var backend = harness.newBackend() + .withConnectedEmbeddedPluginAndEnabledLanguage(TestPlugin.XML) + .withSonarCloudUrl(server.baseUrl()) + .withSonarCloudConnection("connectionId", "organizationKey", true, storage -> storage + .withProject("projectKey", project -> project.withRuleSet("xml", ruleSet -> ruleSet.withActiveRule("xml:S3421", "MAJOR")))) + .withBoundConfigScope("configScope", "connectionId", "projectKey") + .start(fakeClient); + var issue = analyzeFileAndGetIssue(fileUri, fakeClient, backend, "configScope"); + + var fixSuggestion = backend.getAiCodeFixRpcService().suggestFix(new SuggestFixParams("configScope", issue.getId())).join(); + + assertThat(fixSuggestion.getId()).isEqualTo(UUID.fromString("e51b7bbd-72bc-4008-a4f1-d75583f3dc98")); + assertThat(fixSuggestion.getExplanation()).isEqualTo("This is the explanation"); + assertThat(fixSuggestion.getChanges()) + .extracting(SuggestFixChangeDto::getStartLine, SuggestFixChangeDto::getEndLine, SuggestFixChangeDto::getNewCode) + .containsExactly(tuple(0, 0, "This is the new code")); + assertThat(server.getMockServer().getAllServeEvents().get(0).getRequest().getBodyAsString()) + .isEqualTo( + """ + {"organizationKey":"organizationKey","projectKey":"projectKey","issue":{"message":"Replace \\"pom.version\\" with \\"project.version\\".","startLine":6,"endLine":6,"ruleKey":"xml:S3421","sourceCode":"%s"}}""" + .formatted(sourceCode.replace("\\", "\\\\").replace("\n", "\\n").replace("\"", "\\\""))); + } +} diff --git a/medium-tests/src/test/java/utils/MockWebServerExtensionWithProtobuf.java b/medium-tests/src/test/java/utils/MockWebServerExtensionWithProtobuf.java index bcc718fb41..6cee4ee992 100644 --- a/medium-tests/src/test/java/utils/MockWebServerExtensionWithProtobuf.java +++ b/medium-tests/src/test/java/utils/MockWebServerExtensionWithProtobuf.java @@ -79,7 +79,7 @@ public EndpointParams endpointParams() { } public EndpointParams endpointParams(@Nullable String organizationKey) { - return new EndpointParams(url("/"), organizationKey != null, organizationKey); + return new EndpointParams(url("/"), url("/"), organizationKey != null, organizationKey); } } diff --git a/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/SonarLintRpcErrorCode.java b/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/SonarLintRpcErrorCode.java index 5e5a72fe6c..a3474f1692 100644 --- a/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/SonarLintRpcErrorCode.java +++ b/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/SonarLintRpcErrorCode.java @@ -31,4 +31,6 @@ public class SonarLintRpcErrorCode { public static final int HTTP_REQUEST_FAILED = -8; public static final int TASK_EXECUTION_TIMEOUT = -9; public static final int PROGRESS_CREATION_FAILED = -10; + public static final int CONNECTION_KIND_NOT_SUPPORTED = -11; + public static final int FILE_NOT_FOUND = -12; } diff --git a/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/SonarLintRpcServer.java b/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/SonarLintRpcServer.java index cd0ca0e719..2128b63181 100644 --- a/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/SonarLintRpcServer.java +++ b/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/SonarLintRpcServer.java @@ -34,6 +34,7 @@ import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.IssueRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.newcode.NewCodeRpcService; +import org.sonarsource.sonarlint.core.rpc.protocol.backend.remediation.aicodefix.AiCodeFixRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.RulesRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.telemetry.TelemetryRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.TaintVulnerabilityTrackingRpcService; @@ -88,6 +89,9 @@ public interface SonarLintRpcServer { @JsonDelegate DogfoodingRpcService getDogfoodingService(); + @JsonDelegate + AiCodeFixRpcService getAiCodeFixRpcService(); + @JsonRequest CompletableFuture shutdown(); diff --git a/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/backend/remediation/aicodefix/AiCodeFixRpcService.java b/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/backend/remediation/aicodefix/AiCodeFixRpcService.java new file mode 100644 index 0000000000..1b785e01d3 --- /dev/null +++ b/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/backend/remediation/aicodefix/AiCodeFixRpcService.java @@ -0,0 +1,37 @@ +/* + * SonarLint Core - RPC Protocol + * 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. + */ +package org.sonarsource.sonarlint.core.rpc.protocol.backend.remediation.aicodefix; + +import java.util.concurrent.CompletableFuture; +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; + +public interface AiCodeFixRpcService { + /** + * Throws an exception if the issue is not fixable: + *
    + *
  • the configuration scope is not bound
  • + *
  • the configuration scope is bound to SonarQube Server
  • + *
  • the issue is a file-level issue
  • + *
  • the issue rule is not supported
  • + *
+ */ + @JsonRequest + CompletableFuture suggestFix(SuggestFixParams params); +} diff --git a/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/backend/remediation/aicodefix/SuggestFixChangeDto.java b/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/backend/remediation/aicodefix/SuggestFixChangeDto.java new file mode 100644 index 0000000000..c280860151 --- /dev/null +++ b/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/backend/remediation/aicodefix/SuggestFixChangeDto.java @@ -0,0 +1,44 @@ +/* + * SonarLint Core - RPC Protocol + * 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. + */ +package org.sonarsource.sonarlint.core.rpc.protocol.backend.remediation.aicodefix; + +public class SuggestFixChangeDto { + private final int startLine; + private final int endLine; + private final String newCode; + + public SuggestFixChangeDto(int startLine, int endLine, String newCode) { + this.startLine = startLine; + this.endLine = endLine; + this.newCode = newCode; + } + + public int getStartLine() { + return startLine; + } + + public int getEndLine() { + return endLine; + } + + public String getNewCode() { + return newCode; + } +} diff --git a/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/backend/remediation/aicodefix/SuggestFixParams.java b/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/backend/remediation/aicodefix/SuggestFixParams.java new file mode 100644 index 0000000000..cf1747d348 --- /dev/null +++ b/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/backend/remediation/aicodefix/SuggestFixParams.java @@ -0,0 +1,40 @@ +/* + * SonarLint Core - RPC Protocol + * 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. + */ +package org.sonarsource.sonarlint.core.rpc.protocol.backend.remediation.aicodefix; + +import java.util.UUID; + +public class SuggestFixParams { + private final String configurationScopeId; + private final UUID issueId; + + public SuggestFixParams(String configurationScopeId, UUID issueId) { + this.configurationScopeId = configurationScopeId; + this.issueId = issueId; + } + + public String getConfigurationScopeId() { + return configurationScopeId; + } + + public UUID getIssueId() { + return issueId; + } +} diff --git a/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/backend/remediation/aicodefix/SuggestFixResponse.java b/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/backend/remediation/aicodefix/SuggestFixResponse.java new file mode 100644 index 0000000000..dc7ed7bd82 --- /dev/null +++ b/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/backend/remediation/aicodefix/SuggestFixResponse.java @@ -0,0 +1,47 @@ +/* + * SonarLint Core - RPC Protocol + * 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. + */ +package org.sonarsource.sonarlint.core.rpc.protocol.backend.remediation.aicodefix; + +import java.util.List; +import java.util.UUID; + +public class SuggestFixResponse { + private final UUID id; + private final String explanation; + private final List changes; + + public SuggestFixResponse(UUID id, String explanation, List changes) { + this.id = id; + this.explanation = explanation; + this.changes = changes; + } + + public UUID getId() { + return id; + } + + public String getExplanation() { + return explanation; + } + + public List getChanges() { + return changes; + } +} diff --git a/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/backend/remediation/aicodefix/package-info.java b/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/backend/remediation/aicodefix/package-info.java new file mode 100644 index 0000000000..8910ba578e --- /dev/null +++ b/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/backend/remediation/aicodefix/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarLint Core - RPC Protocol + * 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. + */ +@ParametersAreNonnullByDefault +package org.sonarsource.sonarlint.core.rpc.protocol.backend.remediation.aicodefix; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/SonarLintTestRpcServer.java b/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/SonarLintTestRpcServer.java index da1ccd4633..6de3928fab 100644 --- a/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/SonarLintTestRpcServer.java +++ b/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/SonarLintTestRpcServer.java @@ -39,6 +39,7 @@ import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.IssueRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.newcode.NewCodeRpcService; +import org.sonarsource.sonarlint.core.rpc.protocol.backend.remediation.aicodefix.AiCodeFixRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.RulesRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.telemetry.TelemetryRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.TaintVulnerabilityTrackingRpcService; @@ -143,6 +144,11 @@ public DogfoodingRpcService getDogfoodingService() { return serverUsingRpc.getDogfoodingService(); } + @Override + public AiCodeFixRpcService getAiCodeFixRpcService() { + return serverUsingRpc.getAiCodeFixRpcService(); + } + public Path getWorkDir() { return workDir; } diff --git a/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/server/ServerFixture.java b/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/server/ServerFixture.java index 9fedb57973..03c12501ef 100644 --- a/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/server/ServerFixture.java +++ b/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/server/ServerFixture.java @@ -19,6 +19,10 @@ */ package org.sonarsource.sonarlint.core.test.utils.server; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; import com.google.protobuf.Message; @@ -37,6 +41,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.UUID; import java.util.function.Consumer; import java.util.function.UnaryOperator; import java.util.stream.Collectors; @@ -68,6 +73,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.jsonResponse; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; @@ -194,8 +200,7 @@ public ServerBuilder withResponseCode(Integer status) { } public Server start() { - var server = new Server(serverKind, serverStatus, organizationKey, version, projectByProjectKey, smartNotificationsSupported, pluginsByKey, - qualityProfilesByKey, + var server = new Server(serverKind, serverStatus, organizationKey, version, projectByProjectKey, smartNotificationsSupported, pluginsByKey, qualityProfilesByKey, tokensRegistered, statusCode); server.start(); if (onStart != null) { @@ -233,6 +238,7 @@ public static class ServerProjectBuilder { private final List relativeFilePaths = new ArrayList<>(); private String name = "MyProject"; private String projectName; + private AiCodeFixBuilder aiCodeFix; private ServerProjectBuilder() { branchesByName.put(mainBranchName, new ServerProjectBranchBuilder()); @@ -289,6 +295,12 @@ public ServerProjectBuilder withFile(String relativeFilePath) { return this; } + public ServerProjectBuilder withAiCodeFix(UnaryOperator aiCodeFixBuilder) { + this.aiCodeFix = new AiCodeFixBuilder(); + aiCodeFixBuilder.apply(aiCodeFix); + return this; + } + public static class ServerProjectBranchBuilder { protected final Collection hotspots = new ArrayList<>(); protected final Collection issues = new ArrayList<>(); @@ -427,6 +439,37 @@ public String getFilePath() { } } + public static class AiCodeFixBuilder { + private UUID id = UUID.randomUUID(); + private String explanation = "default"; + private final List changes = new ArrayList<>(); + + public AiCodeFixBuilder withId(UUID id) { + this.id = id; + return this; + } + + public AiCodeFixBuilder withExplanation(String explanation) { + this.explanation = explanation; + return this; + } + + public AiCodeFixBuilder withChange(int startLine, int endLine, String newCode) { + this.changes.add(new AiCodeFixChange(startLine, endLine, newCode)); + return this; + } + + public AiCodeFix build() { + return new AiCodeFix(id, explanation, changes); + } + } + + public record AiCodeFix(UUID id, String explanation, List changes) { + } + + public record AiCodeFixChange(int startLine, int endLine, String newCode) { + } + public static class ServerProjectPullRequestBuilder extends ServerProjectBranchBuilder { } @@ -585,6 +628,7 @@ private void registerWebApiResponses() { registerSettingsApiResponses(); registerTokenApiResponse(); registerComponentApiResponses(); + registerFixSuggestionsApiResponses(); } } @@ -1183,6 +1227,19 @@ private void registerTokenApiResponse() { tokenName -> mockServer.stubFor(post("/api/user_tokens/revoke").withRequestBody(WireMock.containing("name=" + tokenName)).willReturn(aResponse().withStatus(statusCode)))); } + private void registerFixSuggestionsApiResponses() { + projectsByProjectKey.forEach((projectKey, project) -> { + if (project.aiCodeFix != null) { + try { + mockServer.stubFor(post("/fix-suggestions/ai-suggestions") + .willReturn(jsonResponse(new ObjectMapper().setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY).writeValueAsString(project.aiCodeFix.build()), 200))); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException(e); + } + } + }); + } + public void shutdown() { mockServer.stop(); }