All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sonarsource.sonarlint.ls.clientapi.SonarLintVSCodeClient Maven / Gradle / Ivy

There is a newer version: 3.12.0.75621
Show newest version
/*
 * SonarLint Language Server
 * Copyright (C) 2009-2024 SonarSource SA
 * mailto:info AT sonarsource DOT com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package org.sonarsource.sonarlint.ls.clientapi;

import java.net.Authenticator;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import nl.altindag.ssl.util.CertificateUtils;
import org.apache.commons.codec.digest.DigestUtils;
import org.eclipse.lsp4j.MessageActionItem;
import org.eclipse.lsp4j.MessageParams;
import org.eclipse.lsp4j.MessageType;
import org.eclipse.lsp4j.ShowMessageRequestParams;
import org.eclipse.lsp4j.jsonrpc.CompletableFutures;
import org.jetbrains.annotations.NotNull;
import org.sonarsource.sonarlint.core.commons.HotspotReviewStatus;
import org.sonarsource.sonarlint.core.commons.SonarLintUserHome;
import org.sonarsource.sonarlint.core.rpc.client.ConfigScopeNotFoundException;
import org.sonarsource.sonarlint.core.rpc.client.SonarLintCancelChecker;
import org.sonarsource.sonarlint.core.rpc.client.SonarLintRpcClientDelegate;
import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionDto;
import org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.TaintVulnerabilityDto;
import org.sonarsource.sonarlint.core.rpc.protocol.client.binding.AssistBindingParams;
import org.sonarsource.sonarlint.core.rpc.protocol.client.binding.AssistBindingResponse;
import org.sonarsource.sonarlint.core.rpc.protocol.client.binding.NoBindingSuggestionFoundParams;
import org.sonarsource.sonarlint.core.rpc.protocol.client.binding.SuggestBindingParams;
import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.AssistCreatingConnectionParams;
import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.AssistCreatingConnectionResponse;
import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.ConnectionSuggestionDto;
import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.SuggestConnectionParams;
import org.sonarsource.sonarlint.core.rpc.protocol.client.event.DidReceiveServerHotspotEvent;
import org.sonarsource.sonarlint.core.rpc.protocol.client.fix.FixSuggestionDto;
import org.sonarsource.sonarlint.core.rpc.protocol.client.hotspot.HotspotDetailsDto;
import org.sonarsource.sonarlint.core.rpc.protocol.client.hotspot.RaisedHotspotDto;
import org.sonarsource.sonarlint.core.rpc.protocol.client.http.GetProxyPasswordAuthenticationResponse;
import org.sonarsource.sonarlint.core.rpc.protocol.client.http.ProxyDto;
import org.sonarsource.sonarlint.core.rpc.protocol.client.http.X509CertificateDto;
import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.IssueDetailsDto;
import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedFindingDto;
import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedIssueDto;
import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.ShowIssueParams;
import org.sonarsource.sonarlint.core.rpc.protocol.client.log.LogParams;
import org.sonarsource.sonarlint.core.rpc.protocol.client.message.ShowSoonUnsupportedMessageParams;
import org.sonarsource.sonarlint.core.rpc.protocol.client.plugin.DidSkipLoadingPluginParams;
import org.sonarsource.sonarlint.core.rpc.protocol.client.progress.ReportProgressParams;
import org.sonarsource.sonarlint.core.rpc.protocol.client.progress.StartProgressParams;
import org.sonarsource.sonarlint.core.rpc.protocol.client.smartnotification.ShowSmartNotificationParams;
import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.TelemetryClientLiveAttributesResponse;
import org.sonarsource.sonarlint.core.rpc.protocol.common.ClientFileDto;
import org.sonarsource.sonarlint.core.rpc.protocol.common.Either;
import org.sonarsource.sonarlint.core.rpc.protocol.common.Language;
import org.sonarsource.sonarlint.core.rpc.protocol.common.TokenDto;
import org.sonarsource.sonarlint.core.rpc.protocol.common.UsernamePasswordDto;
import org.sonarsource.sonarlint.ls.AnalysisHelper;
import org.sonarsource.sonarlint.ls.DiagnosticPublisher;
import org.sonarsource.sonarlint.ls.ForcedAnalysisCoordinator;
import org.sonarsource.sonarlint.ls.SkippedPluginsNotifier;
import org.sonarsource.sonarlint.ls.SonarLintExtendedLanguageClient;
import org.sonarsource.sonarlint.ls.SonarLintExtendedLanguageClient.CreateConnectionParams;
import org.sonarsource.sonarlint.ls.backend.BackendServiceFacade;
import org.sonarsource.sonarlint.ls.commands.ShowAllLocationsCommand;
import org.sonarsource.sonarlint.ls.connected.ProjectBindingManager;
import org.sonarsource.sonarlint.ls.connected.TaintVulnerabilitiesCache;
import org.sonarsource.sonarlint.ls.connected.api.HostInfoProvider;
import org.sonarsource.sonarlint.ls.connected.events.ServerSentEventsHandlerService;
import org.sonarsource.sonarlint.ls.connected.notifications.SmartNotifications;
import org.sonarsource.sonarlint.ls.domain.TaintIssue;
import org.sonarsource.sonarlint.ls.folders.WorkspaceFolderBranchManager;
import org.sonarsource.sonarlint.ls.folders.WorkspaceFolderWrapper;
import org.sonarsource.sonarlint.ls.folders.WorkspaceFoldersManager;
import org.sonarsource.sonarlint.ls.log.LanguageClientLogger;
import org.sonarsource.sonarlint.ls.progress.LSProgressMonitor;
import org.sonarsource.sonarlint.ls.settings.SettingsManager;
import org.sonarsource.sonarlint.ls.standalone.notifications.PromotionalNotifications;
import org.sonarsource.sonarlint.ls.util.Utils;

import static java.lang.String.format;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
import static org.sonarsource.sonarlint.ls.backend.BackendService.ROOT_CONFIGURATION_SCOPE;
import static org.sonarsource.sonarlint.ls.util.URIUtils.getFullFileUriFromFragments;
import static org.sonarsource.sonarlint.ls.util.Utils.convertMessageType;

public class SonarLintVSCodeClient implements SonarLintRpcClientDelegate {

  public static final String SONARLINT_SOURCE = "sonarqube";
  private final SonarLintExtendedLanguageClient client;
  private SettingsManager settingsManager;
  private SmartNotifications smartNotifications;
  private final HostInfoProvider hostInfoProvider;
  private final LanguageClientLogger logOutput;
  private ProjectBindingManager bindingManager;
  private ServerSentEventsHandlerService serverSentEventsHandlerService;
  private BackendServiceFacade backendServiceFacade;
  private WorkspaceFolderBranchManager branchManager;
  private final TaintVulnerabilitiesCache taintVulnerabilitiesCache;
  private WorkspaceFoldersManager workspaceFoldersManager;
  private ForcedAnalysisCoordinator forcedAnalysisCoordinator;
  private DiagnosticPublisher diagnosticPublisher;
  private final ScheduledExecutorService bindingSuggestionsHandler;
  private final SkippedPluginsNotifier skippedPluginsNotifier;
  private final PromotionalNotifications promotionalNotifications;
  private final LSProgressMonitor progressMonitor;


  private AnalysisHelper analysisHelper;


  public SonarLintVSCodeClient(SonarLintExtendedLanguageClient client, HostInfoProvider hostInfoProvider,
    LanguageClientLogger logOutput, TaintVulnerabilitiesCache taintVulnerabilitiesCache,
    SkippedPluginsNotifier skippedPluginsNotifier, PromotionalNotifications promotionalNotifications, LSProgressMonitor progressMonitor) {
    this.client = client;
    this.hostInfoProvider = hostInfoProvider;
    this.logOutput = logOutput;
    this.taintVulnerabilitiesCache = taintVulnerabilitiesCache;
    this.skippedPluginsNotifier = skippedPluginsNotifier;
    this.promotionalNotifications = promotionalNotifications;
    this.progressMonitor = progressMonitor;
    var bindingSuggestionsThreadFactory = Utils.threadFactory("Binding suggestion handler", false);
    bindingSuggestionsHandler = Executors.newSingleThreadScheduledExecutor(bindingSuggestionsThreadFactory);
    initializeDefaultProxyAuthenticator();
  }

  private static void initializeDefaultProxyAuthenticator() {
    Authenticator.setDefault(new SystemPropertiesAuthenticator());
  }

  @Override
  public void suggestBinding(Map> suggestionsByConfigScope) {
    bindingSuggestionsHandler.schedule(() -> {
      var relevantSuggestionsPerConfigScope = new ConcurrentHashMap>();
      suggestionsByConfigScope.forEach((configScopeId, suggestions) -> {
        var maybeBinding = bindingManager.getBinding(URI.create(configScopeId));
        if (maybeBinding.isEmpty()) {
          relevantSuggestionsPerConfigScope.put(configScopeId, suggestions);
        }
      });
      if (!relevantSuggestionsPerConfigScope.isEmpty()) {
        client.suggestBinding(new SuggestBindingParams(relevantSuggestionsPerConfigScope));
      }
    }, 5L, TimeUnit.SECONDS);
  }

  @Override
  public void openUrlInBrowser(URL url) {
    client.browseTo(url.toString());
  }

  @Override
  public void showMessage(org.sonarsource.sonarlint.core.rpc.protocol.client.message.MessageType type, String text) {
    client.showMessage(new MessageParams(convertMessageType(type), text));
  }

  @Override
  public void log(LogParams params) {
    var rawMessage = params.getMessage();
    var sanitizedMessage = rawMessage != null ? rawMessage : "null";
    var level = params.getLevel();
    logOutput.log(sanitizedMessage, level);
    var stackTrace = params.getStackTrace();
    if (stackTrace != null) {
      logOutput.log(stackTrace, level);
    }
  }

  @Override
  public void showSoonUnsupportedMessage(ShowSoonUnsupportedMessageParams coreParams) {
    var clientParams = new SonarLintExtendedLanguageClient.ShowSoonUnsupportedVersionMessageParams(
      coreParams.getDoNotShowAgainId(), coreParams.getText()
    );
    client.showSoonUnsupportedVersionMessage(clientParams);
  }

  @Override
  public void showSmartNotification(ShowSmartNotificationParams showSmartNotificationParams) {
    var connectionOpt = settingsManager.getCurrentSettings().getServerConnections().get(showSmartNotificationParams.getConnectionId());
    if (connectionOpt == null) {
      return;
    }
    smartNotifications.showSmartNotification(showSmartNotificationParams, connectionOpt.isSonarCloudAlias());
  }

  @Override
  public String getClientLiveDescription() {
    return hostInfoProvider.getHostInfo().getDescription();
  }

  @Override
  public void showHotspot(String configurationScopeId, HotspotDetailsDto hotspotDetails) {
    var rule = hotspotDetails.getRule();
    var clientRule = new SonarLintExtendedLanguageClient.ShowHotspotParams.HotspotRule(rule.getKey(), rule.getName(), rule.getSecurityCategory(),
      rule.getVulnerabilityProbability(), rule.getRiskDescription(), rule.getVulnerabilityDescription(), rule.getFixRecommendations());
    var reviewStatus = HotspotReviewStatus.TO_REVIEW.name().equals(hotspotDetails.getStatus()) ? "To Review" : "Reviewed";
    var showHotspotParams = new SonarLintExtendedLanguageClient.ShowHotspotParams(hotspotDetails.getKey(), hotspotDetails.getMessage(),
      hotspotDetails.getIdeFilePath().toString(),
      hotspotDetails.getTextRange(), hotspotDetails.getAuthor(), reviewStatus, hotspotDetails.getResolution(),
      clientRule);
    client.showHotspot(showHotspotParams);
  }

  @Override
  public void showIssue(String folderUri, IssueDetailsDto issueDetails) {
    var maybeBinding = bindingManager.getBindingIfExists(URI.create(folderUri));
    if (maybeBinding.isPresent()) {
      logOutput.debug("Show issue with description");
      var projectBinding = maybeBinding.get();
      client.showIssue(new ShowAllLocationsCommand.Param(new ShowIssueParams(folderUri, issueDetails), projectBinding.connectionId(), true));
    } else {
      logOutput.debug("Show issue without description");
      client.showIssue(new ShowAllLocationsCommand.Param(new ShowIssueParams(folderUri, issueDetails), null, false));
    }
  }

  @Override
  public void showFixSuggestion(String configurationScopeId, String issueKey, FixSuggestionDto fixSuggestion) {
    var textEdits = fixSuggestion.fileEdit().changes();
    var fullFileUri = getFullFileUriFromFragments(configurationScopeId, fixSuggestion.fileEdit().idePath());
    client.showFixSuggestion(new SonarLintExtendedLanguageClient.ShowFixSuggestionParams(fixSuggestion.suggestionId(), textEdits, fullFileUri.toString()));
  }

  @Override
  public AssistCreatingConnectionResponse assistCreatingConnection(AssistCreatingConnectionParams params, SonarLintCancelChecker cancelChecker) {
    var tokenValue = params.getTokenValue();
    var workspaceFoldersFuture = client.workspaceFolders();
    var isSonarCloud = params.getConnectionParams().isRight();
    var assistCreatingConnectionFuture = client.assistCreatingConnection(new CreateConnectionParams(isSonarCloud,
      isSonarCloud ? params.getConnectionParams().getRight().getOrganizationKey() : params.getConnectionParams().getLeft().getServerUrl(),
      tokenValue));
    return workspaceFoldersFuture.thenCombine(assistCreatingConnectionFuture, (workspaceFolders, assistCreatingConnectionResponse) -> {
      var newConnectionId = assistCreatingConnectionResponse.getNewConnectionId();
      if (newConnectionId != null) {
        var serverProductName = isSonarCloud ? "SonarQube Cloud" : "SonarQube Server";
        client.showMessage(new MessageParams(MessageType.Info, format("Connection to %s was successfully created.", serverProductName)));
        return new AssistCreatingConnectionResponse(newConnectionId);
      } else {
        throw new CancellationException("Automatic connection setup was cancelled");
      }
    }).join();
  }

  @Override
  public AssistBindingResponse assistBinding(AssistBindingParams params, SonarLintCancelChecker cancelChecker) {
    workspaceFoldersManager.updateAnalysisReadiness(Set.of(params.getConfigScopeId()), false);
    return client.assistBinding(params)
      .thenApply(response -> {
        var configurationScopeId = response.getConfigurationScopeId();
        var pathParts = configurationScopeId.split("/");
        var projectName = pathParts[pathParts.length - 1];
        client.showMessage(new MessageParams(MessageType.Info, "Project '" + projectName + "' was successfully bound to '" + params.getProjectKey() + "'."));
        return new AssistBindingResponse(configurationScopeId);
      }).join();
  }

  @Override
  public void noBindingSuggestionFound(NoBindingSuggestionFoundParams params) {
    var messageRequestParams = new ShowMessageRequestParams();
    messageRequestParams.setMessage("SonarQube for VS Code couldn't match the server project '" + params.getProjectKey() + "' to any of the currently " +
      "open workspace folders. Please make sure the project is open in the workspace, or try configuring the binding manually.");
    messageRequestParams.setType(MessageType.Error);
    var learnMoreAction = new MessageActionItem("Learn more");
    messageRequestParams.setActions(List.of(learnMoreAction));
    client.showMessageRequest(messageRequestParams)
      .thenAccept(action -> {
        if (learnMoreAction.equals(action)) {
          client.browseTo("https://docs.sonarsource.com/sonarqube-for-ide/vs-code/troubleshooting/#troubleshooting-connected-mode-setup");
        }
      });
  }

  @Override
  public void startProgress(StartProgressParams startProgressParams) {
    progressMonitor.createAndStartProgress(startProgressParams);
  }

  @Override
  public void reportProgress(ReportProgressParams reportProgressParams) {
    if (reportProgressParams.getNotification().isLeft()) {
      progressMonitor.reportProgress(reportProgressParams);
    } else if (reportProgressParams.getNotification().isRight()) {
      progressMonitor.end(reportProgressParams);
    }
  }

  @Override
  public void didSynchronizeConfigurationScopes(Set configurationScopeIds) {
    // no-op
  }

  @Nullable
  @Override
  public Either getCredentials(String connectionId) {
    var connectionSettings = settingsManager.getCurrentSettings().getServerConnections().get(connectionId);
    if (connectionSettings == null) return null;
    var token = connectionSettings.getToken();
    return Either.forLeft(new TokenDto(token));
  }

  @Override
  public TelemetryClientLiveAttributesResponse getTelemetryLiveAttributes() {
    return new TelemetryClientLiveAttributesResponse(backendServiceFacade.getTelemetryInitParams().additionalAttributes());
  }

  @Override
  public List selectProxies(URI uri) {
    var proxies = ProxySelector.getDefault().select(uri);
    return proxies.stream().map(SonarLintVSCodeClient::convert).toList();
  }

  private static ProxyDto convert(Proxy proxy) {
    if (proxy.type() == Proxy.Type.DIRECT) {
      return ProxyDto.NO_PROXY;
    }
    var address = (InetSocketAddress) proxy.address();
    var server = address.getHostString();
    var port = address.getPort();
    return new ProxyDto(proxy.type(), server, port);
  }

  @Override
  public GetProxyPasswordAuthenticationResponse getProxyPasswordAuthentication(String host, int port, String protocol, String prompt, String scheme, URL targetHost) {
    // use null addr, because the authentication fails if it does not exactly match the expected realm's host
    var passwordAuthentication = Authenticator.requestPasswordAuthentication(host, null, port, protocol, prompt, scheme,
      targetHost, Authenticator.RequestorType.PROXY);
    return new GetProxyPasswordAuthenticationResponse(passwordAuthentication != null ? passwordAuthentication.getUserName() : null,
      passwordAuthentication != null ? new String(passwordAuthentication.getPassword()) : null);
  }

  @Override
  public boolean checkServerTrusted(List chain, String authType) {
    var certs = CertificateUtils.parsePemCertificate(chain.get(0).getPem());
    var sha1fingerprint = "";
    var sha256fingerprint = "";
    X509Certificate untrustedCert = null;
    try {
      untrustedCert = (X509Certificate) certs.get(0);
      sha1fingerprint = Utils.formatSha1Fingerprint(DigestUtils.sha1Hex(untrustedCert.getEncoded()));
      sha256fingerprint = Utils.formatSha256Fingerprint(DigestUtils.sha256Hex(untrustedCert.getEncoded()));
    } catch (CertificateEncodingException | IndexOutOfBoundsException e) {
      logOutput.errorWithStackTrace("Certificate encoding is malformed, SHA fingerprints will not be displayed.", e);
    }
    var pathToActualTrustStore = SonarLintUserHome.get().resolve("ssl/truststore.p12");
    var confirmationParams = new SonarLintExtendedLanguageClient.SslCertificateConfirmationParams(
      untrustedCert == null ? "" : untrustedCert.getSubjectX500Principal().getName(),
      untrustedCert == null ? "" : untrustedCert.getIssuerX500Principal().getName(),
      untrustedCert == null ? "" : untrustedCert.getNotBefore().toString(),
      untrustedCert == null ? "" : untrustedCert.getNotAfter().toString(),
      sha1fingerprint,
      sha256fingerprint,
      pathToActualTrustStore.toString()
    );

    return client.askSslCertificateConfirmation(confirmationParams).join();
  }

  @Override
  public void didReceiveServerHotspotEvent(DidReceiveServerHotspotEvent event) {
    serverSentEventsHandlerService.handleHotspotEvent(event);
  }

  @Nullable
  @Override
  public String matchSonarProjectBranch(String configurationScopeId, String mainBranchName, Set allBranchesNames, SonarLintCancelChecker cancelChecker) {
    return branchManager.matchSonarProjectBranch(configurationScopeId, mainBranchName, allBranchesNames, cancelChecker);
  }

  @Override
  public boolean matchProjectBranch(String configurationScopeId, String branchNameToMatch, SonarLintCancelChecker cancelChecker) throws ConfigScopeNotFoundException {
    return branchManager.matchProjectBranch(configurationScopeId, branchNameToMatch, cancelChecker);
  }

  @Override
  public void didChangeMatchedSonarProjectBranch(String configScopeId, String newMatchedBranchName) {
    client.setReferenceBranchNameForFolder(SonarLintExtendedLanguageClient.ReferenceBranchForFolder.of(configScopeId,
      newMatchedBranchName));
  }

  @Override
  public void didChangeTaintVulnerabilities(String folderUri, Set closedTaintVulnerabilityIds,
    List addedTaintVulnerabilities, List updatedTaintVulnerabilities) {
    var addedTaintVulnerabilitiesByFile = addedTaintVulnerabilities.stream()
      .collect(groupingBy(taintVulnerabilityDto -> getFullFileUriFromFragments(folderUri, taintVulnerabilityDto.getIdeFilePath()), toList()));
    var updatedTaintVulnerabilitiesByFile = updatedTaintVulnerabilities.stream()
      .collect(groupingBy(taintVulnerabilityDto -> getFullFileUriFromFragments(folderUri, taintVulnerabilityDto.getIdeFilePath()), toList()));

    // Remove taints that were closed
    taintVulnerabilitiesCache.getTaintVulnerabilitiesPerFile().values().stream().flatMap(Collection::stream)
      .filter(taintIssue -> closedTaintVulnerabilityIds.contains(taintIssue.getId()))
      .forEach(taintIssue -> taintVulnerabilitiesCache.removeTaintIssue(
        getFullFileUriFromFragments(folderUri, taintIssue.getIdeFilePath()).toString(), taintIssue.getSonarServerKey()));

    workspaceFoldersManager.getFolder(URI.create(folderUri))
      .map(workspaceFolderWrapper -> Objects.requireNonNull(bindingManager
        .getServerConnectionSettingsFor(workspaceFolderWrapper.getSettings().getConnectionId())).isSonarCloudAlias())
      .ifPresent(isSonarCloud -> updateTaintVulnerabilitiesCache(addedTaintVulnerabilitiesByFile, updatedTaintVulnerabilitiesByFile, folderUri, isSonarCloud));
  }

  private void updateTaintVulnerabilitiesCache(Map> addedTaints, Map> updateTaints,
    String folderUri, boolean isSonarCloud) {
    var existingTaintVulnerabilitiesPerFile = taintVulnerabilitiesCache.getTaintVulnerabilitiesPerFile();

    // add new ones
    handleAddedTaints(addedTaints, folderUri, isSonarCloud, existingTaintVulnerabilitiesPerFile);

    // update existing ones
    handleUpdatedTaints(updateTaints, folderUri, isSonarCloud, existingTaintVulnerabilitiesPerFile);
  }

  private void handleAddedTaints(Map> addedTaints, String folderUri, boolean isSonarCloud,
    Map> existingTaintVulnerabilitiesPerFile) {
    addedTaints.forEach((fileUri, added) -> {
      var addedTaintIssuesForFile = dtosToTaintIssues(folderUri, added, isSonarCloud);
      if (existingTaintVulnerabilitiesPerFile.containsKey(fileUri)) {
        addedTaintIssuesForFile.addAll(existingTaintVulnerabilitiesPerFile.get(fileUri));
      }
      taintVulnerabilitiesCache.reload(fileUri, addedTaintIssuesForFile);
      diagnosticPublisher.publishDiagnostics(fileUri, true);
    });
  }

  private void handleUpdatedTaints(Map> updateTaints, String folderUri, boolean isSonarCloud,
    Map> existingTaintVulnerabilitiesPerFile) {
    updateTaints.forEach((fileUri, updates) -> {
      if (existingTaintVulnerabilitiesPerFile.containsKey(fileUri)) {
        updates.forEach(dto -> {
          if (taintVulnerabilitiesCache.getTaintVulnerabilityByKey(dto.getSonarServerKey()).isPresent()) {
            taintVulnerabilitiesCache.removeTaintIssue(fileUri.toString(), dto.getSonarServerKey());
          }
          if (!dto.isResolved()) {
            taintVulnerabilitiesCache.add(fileUri, new TaintIssue(dto, folderUri, isSonarCloud));
          }
        });
      } else {
        taintVulnerabilitiesCache.reload(fileUri, dtosToTaintIssues(folderUri, updates, isSonarCloud));
      }
      diagnosticPublisher.publishDiagnostics(fileUri, true);
    });
  }

  @Override
  public List listFiles(String configScopeId) {
    return configScopeIdAsUri(configScopeId)
      .map(configScopeIdAsUri -> CompletableFutures.computeAsync(c -> {
        var response = client.listFilesInFolder(new SonarLintExtendedLanguageClient.FolderUriParams(configScopeId)).join();
        var folderPath = Path.of(configScopeIdAsUri);
        return response.getFoundFiles().stream()
          .map(file -> {
            var filePath = Path.of(file.getFilePath());
            return new ClientFileDto(filePath.toUri(), folderPath.relativize(filePath), configScopeId, null, StandardCharsets.UTF_8.name(), filePath,
              file.getContent(), null, true);
          })
          .toList();
      }).join())
      .orElse(List.of());
  }

  private static Optional configScopeIdAsUri(String configScopeId) {
    try {
      return Optional.of(URI.create(configScopeId));
    } catch (IllegalArgumentException e) {
      return Optional.empty();
    }
  }

  @Override
  public void didChangeAnalysisReadiness(Set configurationScopeIds, boolean areReadyForAnalysis) {
    workspaceFoldersManager.updateAnalysisReadiness(configurationScopeIds, areReadyForAnalysis);
    if (areReadyForAnalysis) {
      // for each open configScopeId do didOpen for each open file
      Collection all = workspaceFoldersManager.getAll();
      all.forEach(folderWrapper -> {
        var folderUri = folderWrapper.getUri();

        CompletableFutures.computeAsync(cancelChecker -> {
          getNewCodeDefinitionAndSubmitToClient(folderUri.toString());
          return null;
        });

        forcedAnalysisCoordinator.analyzeAllUnboundOpenFiles();
      });
      initializeTaintCache(configurationScopeIds);
    }
  }

  private void initializeTaintCache(Set configurationScopeIds) {
    configurationScopeIds.forEach(configurationScopeId -> {
      if (Objects.equals(configurationScopeId, ROOT_CONFIGURATION_SCOPE)) {
        return;
      }
      var binding = bindingManager.getBinding(URI.create(configurationScopeId));
      if (binding.isPresent()) {
        var isSonarCloud = Objects.requireNonNull(bindingManager.getServerConnectionSettingsFor(binding.get().connectionId())).isSonarCloudAlias();
        CompletableFutures.computeAsync(cancelChecker -> {
          var taints = backendServiceFacade.getBackendService().getAllTaints(configurationScopeId).join();

          var taintsByFile = taints.getTaintVulnerabilities()
            .stream()
            .collect(groupingBy(taintVulnerabilityDto ->
              getFullFileUriFromFragments(configurationScopeId, taintVulnerabilityDto.getIdeFilePath()), toList()));

          taintsByFile.forEach((fileUri, t) -> {
            var vulnerabilities = dtosToTaintIssues(configurationScopeId, t, isSonarCloud);
            taintVulnerabilitiesCache.reload(fileUri, vulnerabilities);
            diagnosticPublisher.publishDiagnostics(fileUri, true);
          });

          return null;
        });
      }
    });
  }

  @Override
  public void suggestConnection(Map> configScopesToConnectionSuggestions) {
    if (configScopesToConnectionSuggestions.isEmpty()) {
      return;
    }
    client.suggestConnection(new SuggestConnectionParams(configScopesToConnectionSuggestions));
  }

  @NotNull
  private static ArrayList dtosToTaintIssues(String configurationScopeId, List t, Boolean isSonarCloud) {
    return t.stream()
      .map(dto ->
        new TaintIssue(dto, configurationScopeId, isSonarCloud))
      .filter(tv -> !tv.isResolved())
      .collect(Collectors.toCollection(ArrayList::new));
  }

  public void setSettingsManager(SettingsManager settingsManager) {
    this.settingsManager = settingsManager;
  }

  public void setBindingManager(ProjectBindingManager bindingManager) {
    this.bindingManager = bindingManager;
  }

  public void setSmartNotifications(SmartNotifications smartNotifications) {
    this.smartNotifications = smartNotifications;
  }

  public void setServerSentEventsHandlerService(ServerSentEventsHandlerService serverSentEventsHandlerService) {
    this.serverSentEventsHandlerService = serverSentEventsHandlerService;
  }

  public void setBackendServiceFacade(BackendServiceFacade backendServiceFacade) {
    this.backendServiceFacade = backendServiceFacade;
  }

  private void getNewCodeDefinitionAndSubmitToClient(String folderUri) {
    backendServiceFacade.getBackendService().getNewCodeDefinition(folderUri)
      .handle((response, e) -> {
        if (e != null) {
          return new SonarLintExtendedLanguageClient.SubmitNewCodeDefinitionParams(folderUri, response.getDescription(), false);
        }
        return new SonarLintExtendedLanguageClient.SubmitNewCodeDefinitionParams(folderUri, response.getDescription(), response.isSupported());
      })
      .thenAccept(client::submitNewCodeDefinition);
  }

  public void setBranchManager(WorkspaceFolderBranchManager branchManager) {
    this.branchManager = branchManager;
  }

  public void setAnalysisTaskExecutor(AnalysisHelper analysisTaskExecutor) {
    this.analysisHelper = analysisTaskExecutor;
  }

  public void setWorkspaceFoldersManager(WorkspaceFoldersManager workspaceFoldersManager) {
    this.workspaceFoldersManager = workspaceFoldersManager;
  }

  public void setAnalysisScheduler(ForcedAnalysisCoordinator analysisScheduler) {
    this.forcedAnalysisCoordinator = analysisScheduler;
  }

  public void setDiagnosticPublisher(DiagnosticPublisher diagnosticPublisher) {
    this.diagnosticPublisher = diagnosticPublisher;
  }

  @Override
  public void didSkipLoadingPlugin(String configurationScopeId, Language language,
    DidSkipLoadingPluginParams.SkipReason reason, String minVersion, @Nullable String currentVersion) {
    skippedPluginsNotifier.notifyOnceForSkippedPlugins(language, reason, minVersion, currentVersion);
  }

  @Override
  public void didDetectSecret(String configScopeId) {
    diagnosticPublisher.didDetectSecret();
  }

  @Override
  public void promoteExtraEnabledLanguagesInConnectedMode(String configurationScopeId, Set languagesToPromote) {
    promotionalNotifications.promoteExtraEnabledLanguagesInConnectedMode(languagesToPromote);
  }

  @Override
  public Path getBaseDir(String configurationScopeId) {
    try {
      return Paths.get(URI.create(configurationScopeId));
    } catch (IllegalArgumentException e) {
      return null;
    }
  }

  @Override
  public void raiseIssues(String configurationScopeId, Map> issuesByFileUri,
    boolean isIntermediatePublication, @Nullable UUID analysisId) {
    var findings = issuesByFileUri.entrySet().stream()
      .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().stream()
        .map(i -> (RaisedFindingDto) i)
        .toList()));
    analysisHelper.handleIssues(findings);
  }

  @Override
  public void raiseHotspots(String configurationScopeId, Map> hotspotsByFileUri,
    boolean isIntermediatePublication, @Nullable UUID analysisId) {
    analysisHelper.handleHotspots(hotspotsByFileUri);
  }

  @Override
  public Set getFileExclusions(String configurationScopeId) {
    var excludes = settingsManager.getCurrentSettings().getAnalysisExcludes();
    return excludes.isEmpty() ? Collections.emptySet() : Arrays.stream(excludes.split(","))
      .collect(Collectors.toSet());
  }

  @Override
  public Map getInferredAnalysisProperties(String configurationScopeId, List filesToAnalyze) {
    return analysisHelper.getInferredAnalysisProperties(configurationScopeId, filesToAnalyze);
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy