Skip to content

Commit

Permalink
SLCORE-1159 Expose if issue is fixable when feature is enabled
Browse files Browse the repository at this point in the history
  • Loading branch information
damien-urruty-sonarsource authored and eray-felek-sonarsource committed Feb 21, 2025
1 parent b28933b commit 394bfa1
Show file tree
Hide file tree
Showing 26 changed files with 611 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ public ValidateConnectionResponse validateConnection(Either<TransientSonarQubeCo
if (validateCredentials.success() && transientConnection.isRight()) {
var organizationKey = transientConnection.getRight().getOrganization();
if (organizationKey != null) {
var organization = serverApi.organization().getOrganization(organizationKey, cancelMonitor);
var organization = serverApi.organization().searchOrganization(organizationKey, cancelMonitor);
if (organization.isEmpty()) {
return new ValidateConnectionResponse(false, "No organizations found for key: " + organizationKey);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public List<OrganizationDto> listUserOrganizations(Either<TokenDto, UsernamePass
@CheckForNull
public OrganizationDto getOrganization(Either<TokenDto, UsernamePasswordDto> credentials, String organizationKey, SonarCloudRegion region, SonarLintCancelMonitor cancelMonitor) {
var helper = connectionManager.getForSonarCloudNoOrg(credentials, region);
var serverOrganization = helper.organization().getOrganization(organizationKey, cancelMonitor);
var serverOrganization = helper.organization().searchOrganization(organizationKey, cancelMonitor);
return serverOrganization.map(o -> new OrganizationDto(o.getKey(), o.getName(), o.getDescription())).orElse(null);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

public enum SonarCloudRegion {
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/");
US("https://us-sc-staging.io", "https://api.us.sonarcloud.io", "wss://events-api.us.sonarcloud.io/");

private final URI productionUri;
private final URI apiProductionUri;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ public SuggestFixResponse suggestFix(String configurationScopeId, UUID issueId,

public Optional<AiCodeFixFeature> getFeature(Binding binding) {
return storageService.connection(binding.connectionId()).aiCodeFix().read()
.filter(settings -> settings.isFeatureEnabled(binding.sonarProjectKey()))
.map(AiCodeFixFeature::new);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
import org.sonarsource.sonarlint.core.serverapi.exception.UnauthorizedException;
import org.sonarsource.sonarlint.core.serverconnection.AiCodeFixSettingsSynchronizer;
import org.sonarsource.sonarlint.core.serverconnection.LocalStorageSynchronizer;
import org.sonarsource.sonarlint.core.serverconnection.OrganizationSynchronizer;
import org.sonarsource.sonarlint.core.serverconnection.ServerInfoSynchronizer;
import org.sonarsource.sonarlint.core.serverconnection.SonarServerSettingsChangedEvent;
import org.sonarsource.sonarlint.core.storage.StorageService;
Expand Down Expand Up @@ -308,7 +309,7 @@ private void synchronizeConnectionAndProjectsIfNeededSync(String connectionId, S
var storage = storageService.connection(connectionId);
var serverInfoSynchronizer = new ServerInfoSynchronizer(storage);
var storageSynchronizer = new LocalStorageSynchronizer(enabledLanguagesToSync, connectedModeEmbeddedPluginKeys, serverInfoSynchronizer, storage);
var aiCodeFixSynchronizer = new AiCodeFixSettingsSynchronizer(storage);
var aiCodeFixSynchronizer = new AiCodeFixSettingsSynchronizer(storage, new OrganizationSynchronizer(storage));
try {
LOG.debug("Synchronizing storage of connection '{}'", connectionId);
var summary = storageSynchronizer.synchronizeServerInfosAndPlugins(serverApi, cancelMonitor);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
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.UrlUtils;
import org.sonarsource.sonarlint.core.serverapi.exception.UnexpectedBodyException;

import static org.sonarsource.sonarlint.core.http.HttpClient.JSON_CONTENT_TYPE;
Expand Down Expand Up @@ -56,4 +57,13 @@ public SupportedRulesResponseDto getSupportedRules(SonarLintCancelMonitor cancel
throw new UnexpectedBodyException(e);
}
}

public OrganizationConfigsResponseDto getOrganizationConfigs(String organizationId, SonarLintCancelMonitor cancelMonitor) {
try (var response = helper.apiGet("/fix-suggestions/organization-configs/" + UrlUtils.urlEncode(organizationId), cancelMonitor)) {
return new Gson().fromJson(response.bodyAsString(), OrganizationConfigsResponseDto.class);
} catch (Exception e) {
LOG.error("Error while fetching the AI CodeFix organization config", e);
throw new UnexpectedBodyException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -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;

import java.util.Set;

public record OrganizationConfigsResponseDto(String organizationId, boolean organizationEligible, SuggestionFeatureEnablement enablement, Set<String> enabledProjectKeys) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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 enum SuggestionFeatureEnablement {
DISABLED,
ENABLED_FOR_ALL_PROJECTS,
ENABLED_FOR_SOME_PROJECTS
}
Original file line number Diff line number Diff line change
@@ -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.organization;

import java.util.UUID;

public record GetOrganizationsResponseDto(String id, UUID uuidV4) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,20 @@
*/
package org.sonarsource.sonarlint.core.serverapi.organization;

import com.google.gson.Gson;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
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.UrlUtils;
import org.sonarsource.sonarlint.core.serverapi.exception.UnexpectedBodyException;
import org.sonarsource.sonarlint.core.serverapi.proto.sonarcloud.ws.Organizations;

public class OrganizationApi {
private static final SonarLintLogger LOG = SonarLintLogger.get();

private final ServerApiHelper helper;

public OrganizationApi(ServerApiHelper helper) {
Expand All @@ -38,13 +43,23 @@ public List<ServerOrganization> listUserOrganizations(SonarLintCancelMonitor can
return fetchUserOrganizations(cancelMonitor);
}

public Optional<ServerOrganization> getOrganization(String organizationKey, SonarLintCancelMonitor cancelMonitor) {
public Optional<ServerOrganization> searchOrganization(String organizationKey, SonarLintCancelMonitor cancelMonitor) {
var url = "api/organizations/search.protobuf?organizations=" + UrlUtils.urlEncode(organizationKey);
return getPaginatedOrganizations(url, cancelMonitor)
.stream()
.findFirst();
}

public GetOrganizationsResponseDto getOrganizationByKey(SonarLintCancelMonitor cancelMonitor) {
var organizationKey = helper.getOrganizationKey().orElseThrow(() -> new IllegalArgumentException("Organizations are only supported for SonarQube Cloud"));
try (var response = helper.apiGet("/organizations/organizations?organizationKey=" + UrlUtils.urlEncode(organizationKey) + "&excludeEligibility=true", cancelMonitor)) {
return new Gson().fromJson(response.bodyAsString(), GetOrganizationsResponseDto[].class)[0];
} catch (Exception e) {
LOG.error("Error while fetching the organization", e);
throw new UnexpectedBodyException(e);
}
}

private List<ServerOrganization> fetchUserOrganizations(SonarLintCancelMonitor cancelMonitor) {
var url = "api/organizations/search.protobuf?member=true";
return getPaginatedOrganizations(url, cancelMonitor);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,35 @@ void it_should_return_the_list_of_supported_rules() {
}
}

@Nested
class GetOrganizationConfigs {

@Test
void it_should_throw_an_exception_if_the_body_is_malformed() {
mockServer.addStringResponse("/fix-suggestions/organization-configs/orgId", """
[
""");

var throwable = catchThrowable(() -> underTest.getOrganizationConfigs("orgId", new SonarLintCancelMonitor()));

assertThat(throwable).isInstanceOf(UnexpectedBodyException.class);
}

@Test
void it_should_return_the_organization_config() {
mockServer.addStringResponse("/fix-suggestions/organization-configs/orgId", """
{
"organizationId": "orgId",
"enablement": "DISABLED",
"organizationEligible": true
}
""");

var response = underTest.getOrganizationConfigs("orgId", new SonarLintCancelMonitor());

assertThat(response)
.isEqualTo(new OrganizationConfigsResponseDto("orgId", true, SuggestionFeatureEnablement.DISABLED, null));
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
package org.sonarsource.sonarlint.core.serverapi.organization;

import java.util.List;
import java.util.UUID;
import java.util.stream.IntStream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
Expand All @@ -28,12 +29,14 @@
import org.sonarsource.sonarlint.core.http.HttpClientProvider;
import org.sonarsource.sonarlint.core.serverapi.MockWebServerExtensionWithProtobuf;
import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper;
import org.sonarsource.sonarlint.core.serverapi.exception.UnexpectedBodyException;
import org.sonarsource.sonarlint.core.serverapi.proto.sonarcloud.ws.Organizations;
import org.sonarsource.sonarlint.core.serverapi.proto.sonarcloud.ws.Organizations.Organization;
import org.sonarsource.sonarlint.core.serverapi.proto.sonarcloud.ws.Organizations.SearchWsResponse;
import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Common.Paging;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;

class OrganizationApiTests {
@RegisterExtension
Expand All @@ -56,7 +59,7 @@ void testListUserOrganizationWithMoreThan20Pages() {
}

@Test
void should_get_organization_details() {
void should_search_organization_details() {
mockServer.addProtobufResponse("/api/organizations/search.protobuf?organizations=org%3Akey&ps=500&p=1", SearchWsResponse.newBuilder()
.addOrganizations(Organization.newBuilder()
.setKey("orgKey")
Expand All @@ -67,7 +70,7 @@ void should_get_organization_details() {
mockServer.addProtobufResponse("/api/organizations/search.protobuf?organizations=org%3Akey&ps=500&p=2", SearchWsResponse.newBuilder().build());
var underTest = new OrganizationApi(new ServerApiHelper(mockServer.endpointParams(), HttpClientProvider.forTesting().getHttpClient()));

var organization = underTest.getOrganization("org:key", new SonarLintCancelMonitor());
var organization = underTest.searchOrganization("org:key", new SonarLintCancelMonitor());

assertThat(organization).hasValueSatisfying(org -> {
assertThat(org.getKey()).isEqualTo("orgKey");
Expand All @@ -76,6 +79,36 @@ void should_get_organization_details() {
});
}

@Test
void should_get_organization_by_key() {
mockServer.addStringResponse("/organizations/organizations?organizationKey=org%3Akey&excludeEligibility=true", """
[{
"id": "orgId",
"uuidV4": "f9cb252d-9f81-4e40-8b77-99fa13190b74"
}]
""");
var underTest = new OrganizationApi(new ServerApiHelper(mockServer.endpointParams("org:key"), HttpClientProvider.forTesting().getHttpClient()));

var organization = underTest.getOrganizationByKey(new SonarLintCancelMonitor());

assertThat(organization)
.isEqualTo(new GetOrganizationsResponseDto("orgId", UUID.fromString("f9cb252d-9f81-4e40-8b77-99fa13190b74")));
}

@Test
void should_throw_if_get_organization_by_key_is_malformed() {
mockServer.addStringResponse("/organizations/organizations?organizationKey=org%3Akey&excludeEligibility=true", """
[{
"id": "orgId",
"uuidV4": "f9cb252d-
""");
var underTest = new OrganizationApi(new ServerApiHelper(mockServer.endpointParams("org:key"), HttpClientProvider.forTesting().getHttpClient()));

var throwable = catchThrowable(() -> underTest.getOrganizationByKey(new SonarLintCancelMonitor()));

assertThat(throwable).isInstanceOf(UnexpectedBodyException.class);
}

private void mockOrganizationsPage(int page, int total) {
List<Organization> orgs = IntStream.rangeClosed(1, 500)
.mapToObj(i -> Organization.newBuilder().setKey("org_page" + page + "number" + i).build())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* SonarLint Core - Server Connection
* 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.serverconnection;

public enum AiCodeFixFeatureEnablement {
DISABLED,
ENABLED_FOR_ALL_PROJECTS,
ENABLED_FOR_SOME_PROJECTS
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,9 @@

import java.util.Set;

public record AiCodeFixSettings(Set<String> supportedRules) {
public record AiCodeFixSettings(Set<String> supportedRules, boolean isOrganizationEligible, AiCodeFixFeatureEnablement enablement, Set<String> enabledProjectKeys) {
public boolean isFeatureEnabled(String projectKey) {
return isOrganizationEligible && (enablement.equals(AiCodeFixFeatureEnablement.ENABLED_FOR_ALL_PROJECTS)
|| (enablement.equals(AiCodeFixFeatureEnablement.ENABLED_FOR_SOME_PROJECTS) && enabledProjectKeys.contains(projectKey)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,21 @@ public class AiCodeFixSettingsSynchronizer {
private static final SonarLintLogger LOG = SonarLintLogger.get();

private final ConnectionStorage storage;
private final OrganizationSynchronizer organizationSynchronizer;

public AiCodeFixSettingsSynchronizer(ConnectionStorage storage) {
public AiCodeFixSettingsSynchronizer(ConnectionStorage storage, OrganizationSynchronizer organizationSynchronizer) {
this.storage = storage;
this.organizationSynchronizer = organizationSynchronizer;
}

public void synchronize(ServerApi serverApi, SonarLintCancelMonitor cancelMonitor) {
if (serverApi.isSonarCloud()) {
try {
var supportedRules = serverApi.fixSuggestions().getSupportedRules(cancelMonitor);
storage.aiCodeFix().store(new AiCodeFixSettings(supportedRules.rules()));
var organization = organizationSynchronizer.readOrSynchronizeOrganization(serverApi, cancelMonitor);
var organizationConfig = serverApi.fixSuggestions().getOrganizationConfigs(organization.id(), cancelMonitor);
storage.aiCodeFix().store(new AiCodeFixSettings(supportedRules.rules(), organizationConfig.organizationEligible(),
AiCodeFixFeatureEnablement.valueOf(organizationConfig.enablement().name()), organizationConfig.enabledProjectKeys()));
} catch (Exception e) {
LOG.error("Error synchronizing AI CodeFix settings", e);
}
Expand Down
Loading

0 comments on commit 394bfa1

Please sign in to comment.