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

org.sonarsource.sonarlint.ls.AnalysisScheduler Maven / Gradle / Ivy

There is a newer version: 3.12.0.75621
Show newest version
/*
 * SonarLint Language Server
 * Copyright (C) 2009-2023 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;

import com.google.common.annotations.VisibleForTesting;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicLong;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonarsource.sonarlint.ls.connected.ProjectBindingManager;
import org.sonarsource.sonarlint.ls.file.OpenFilesCache;
import org.sonarsource.sonarlint.ls.file.VersionedOpenFile;
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.notebooks.OpenNotebooksCache;
import org.sonarsource.sonarlint.ls.notebooks.VersionedOpenNotebook;
import org.sonarsource.sonarlint.ls.settings.WorkspaceFolderSettings;
import org.sonarsource.sonarlint.ls.settings.WorkspaceFolderSettingsChangeListener;
import org.sonarsource.sonarlint.ls.settings.WorkspaceSettings;
import org.sonarsource.sonarlint.ls.settings.WorkspaceSettingsChangeListener;
import org.sonarsource.sonarlint.ls.util.Utils;

import static java.lang.String.format;
import static java.util.stream.Collectors.toSet;

/**
 * Responsible to manage all analyses scheduling
 */
public class AnalysisScheduler implements WorkspaceSettingsChangeListener, WorkspaceFolderSettingsChangeListener {

  private static final CompletableFuture COMPLETED_FUTURE = CompletableFuture.completedFuture(null);
  private static final int DEFAULT_TIMER_MS = 2000;

  static final String SONARLINT_SOURCE = "sonarlint";
  public static final String SONARQUBE_TAINT_SOURCE = "Latest SonarQube Analysis";
  public static final String SONARCLOUD_TAINT_SOURCE = "Latest SonarCloud Analysis";

  public static final String ITEM_LOCATION = "location";
  public static final String ITEM_FLOW = "flow";

  private final OpenFilesCache openFilesCache;
  private final OpenNotebooksCache openNotebooksCache;

  // entries in this set mean that the file is "dirty"
  private final Set events = new HashSet<>();
  private final AtomicLong oldestEvent = new AtomicLong(Long.MAX_VALUE);

  private final WorkspaceFoldersManager workspaceFoldersManager;
  private final ProjectBindingManager bindingManager;
  private final EventWatcher watcher;
  private final LanguageClientLogger lsLogOutput;
  private final AnalysisTaskExecutor analysisTaskExecutor;
  private final SonarLintExtendedLanguageClient client;

  private final ExecutorService asyncExecutor;

  public AnalysisScheduler(LanguageClientLogger lsLogOutput, WorkspaceFoldersManager workspaceFoldersManager, ProjectBindingManager bindingManager, OpenFilesCache openFilesCache,
    OpenNotebooksCache openNotebooksCache, AnalysisTaskExecutor analysisTaskExecutor, SonarLintExtendedLanguageClient client) {
    this(lsLogOutput, workspaceFoldersManager, bindingManager, openFilesCache, openNotebooksCache, analysisTaskExecutor, DEFAULT_TIMER_MS, client);
  }

  @VisibleForTesting
  AnalysisScheduler(LanguageClientLogger lsLogOutput, WorkspaceFoldersManager workspaceFoldersManager, ProjectBindingManager bindingManager, OpenFilesCache openFilesCache,
    OpenNotebooksCache openNotebooksCache, AnalysisTaskExecutor analysisTaskExecutor, long analysisTimerMs, SonarLintExtendedLanguageClient client) {
    this.lsLogOutput = lsLogOutput;
    this.workspaceFoldersManager = workspaceFoldersManager;
    this.bindingManager = bindingManager;
    this.openFilesCache = openFilesCache;
    this.openNotebooksCache = openNotebooksCache;
    this.analysisTaskExecutor = analysisTaskExecutor;
    this.asyncExecutor = Executors.newSingleThreadExecutor(Utils.threadFactory("SonarLint Language Server Analysis Scheduler", false));
    this.watcher = new EventWatcher(analysisTimerMs);
    this.client = client;
  }

  public void didOpen(VersionedOpenFile file) {
    analyzeAsync(AnalysisParams.newAnalysisParams(List.of(file)).withFetchServerIssues());
  }

  public void didChange(URI fileUri) {
    recordEvent(fileUri);
  }

  public void didReceiveHotspotEvent(URI fileUri) {
    recordEvent(fileUri);
  }

  private void recordEvent(URI fileUri) {
    oldestEvent.compareAndSet(Long.MAX_VALUE, System.currentTimeMillis());
    events.add(fileUri);
  }

  private class EventWatcher extends Thread {
    private Future onChangeCurrentTask = COMPLETED_FUTURE;
    private boolean stop = false;
    private final long analysisTimerMs;

    EventWatcher(long analysisTimerMs) {
      this.analysisTimerMs = analysisTimerMs;
      this.setDaemon(true);
      this.setName("sonarlint-auto-trigger");
    }

    public void stopWatcher() {
      stop = true;
      onChangeCurrentTask.cancel(false);
      this.interrupt();
    }

    @Override
    public void run() {
      while (!stop) {
        checkTimers();
        try {
          Thread.sleep(this.analysisTimerMs / 10);
        } catch (InterruptedException e) {
          // continue until stop flag is set
        }
      }
    }

    private void checkTimers() {
      long now = System.currentTimeMillis();
      long oldestEventTimeMs = oldestEvent.get();
      if (now > oldestEventTimeMs + analysisTimerMs && !events.isEmpty()) {
        var it = events.iterator();
        var filesToTrigger = new ArrayList();
        while (it.hasNext()) {
          var e = it.next();
          openFilesCache.getFile(e).ifPresent(filesToTrigger::add);
          openNotebooksCache.getFile(e).ifPresent(notebook -> filesToTrigger.add(notebook.asVersionedOpenFile()));
        }
        triggerFiles(filesToTrigger);
        oldestEvent.set(Long.MAX_VALUE);
      }
    }

    private void triggerFiles(List filesToTrigger) {
      if (!filesToTrigger.isEmpty()) {
        if (!onChangeCurrentTask.isDone()) {
          lsLogOutput.debug("Attempt to cancel previous analysis...");
          onChangeCurrentTask.cancel(false);
          // Wait for the next loop of EventWatcher to recheck if task has been successfully cancelled and then trigger the analysis
          return;
        }
        filesToTrigger.forEach(f -> events.remove(f.getUri()));
        onChangeCurrentTask = analyzeAsync(AnalysisParams.newAnalysisParams(filesToTrigger));
      }
    }
  }

  public void didClose(URI fileUri) {
    events.remove(fileUri);
  }

  public static class AnalysisParams {
    private final List files;
    private final boolean shouldFetchServerIssues;
    private final boolean shouldKeepHotspotsOnly;
    private final boolean shouldShowProgress;

    private AnalysisParams(
      List files,
      boolean shouldFetchServerIssues,
      boolean shouldKeepHotspotsOnly,
      boolean shouldShowProgress
    ) {
      this.files = List.copyOf(files);
      this.shouldFetchServerIssues = shouldFetchServerIssues;
      this.shouldKeepHotspotsOnly = shouldKeepHotspotsOnly;
      this.shouldShowProgress = shouldShowProgress;
    }

    static AnalysisParams newAnalysisParams(List files) {
      return new AnalysisParams(files, false, false, false);
    }

    AnalysisParams withFetchServerIssues() {
      return new AnalysisParams(files, true, shouldKeepHotspotsOnly, shouldShowProgress);
    }

    AnalysisParams withOnlyHotspots() {
      return new AnalysisParams(files, shouldFetchServerIssues, true, shouldShowProgress);
    }

    AnalysisParams withProgress() {
      return new AnalysisParams(files, shouldFetchServerIssues, shouldKeepHotspotsOnly, true);
    }
  }

  /**
   * Handle analysis asynchronously to not block client events for too long
   */
  Future analyzeAsync(AnalysisParams params) {
    var trueFileUris = params.files.stream().filter(f -> {
      if (!Utils.uriHasFileScheme(f.getUri())) {
        lsLogOutput.warn(format("URI '%s' is not in local filesystem, analysis not supported", f.getUri()));
        return false;
      }
      return true;
    }).collect(toSet());
    if (trueFileUris.isEmpty()) {
      return COMPLETED_FUTURE;
    }
    if (trueFileUris.size() == 1) {
      VersionedOpenFile openFile = trueFileUris.iterator().next();
      lsLogOutput.debug(format("Queuing analysis of file \"%s\" (version %d)", openFile.getUri(), openFile.getVersion()));
    } else {
      lsLogOutput.debug(format("Queuing analysis of %d files", trueFileUris.size()));
    }
    var task = new AnalysisTask(trueFileUris, params.shouldFetchServerIssues, params.shouldKeepHotspotsOnly, params.shouldShowProgress);
    var future = asyncExecutor.submit(() -> analysisTaskExecutor.run(task));
    task.setFuture(future);
    return future;
  }

  public void initialize() {
    watcher.start();
  }

  public void shutdown() {
    watcher.stopWatcher();
    events.clear();
    Utils.shutdownAndAwait(asyncExecutor, true);
  }

  public void analyzeAllOpenFilesInFolder(@Nullable WorkspaceFolderWrapper folder) {
    var openedFileUrisInFolder = openFilesCache.getAll().stream()
      .filter(f -> belongToFolder(folder, f.getUri()))
      .toList();
    analyseNotIgnoredFiles(openedFileUrisInFolder);
  }

  private void analyseNotIgnoredFiles(List files) {
    var uriStrings = files.stream().map(it -> it.getUri().toString()).toList();
    var fileUrisParams = new SonarLintExtendedLanguageClient.FileUrisParams(uriStrings);
    client.filterOutExcludedFiles(fileUrisParams)
      .thenAccept(notIgnoredFileUris -> {
        var notIgnoredFiles = files
          .stream().filter(it -> notIgnoredFileUris.getFileUris().contains(it.getUri().toString()))
          .toList();
        analyzeAsync(AnalysisParams.newAnalysisParams(notIgnoredFiles));
      });
  }

  private boolean belongToFolder(WorkspaceFolderWrapper folder, URI fileUri) {
    var actualFolder = workspaceFoldersManager.findFolderForFile(fileUri);
    return (actualFolder.map(f -> f.equals(folder)).orElse(folder == null));
  }

  @Override
  public void onChange(@CheckForNull WorkspaceSettings oldValue, WorkspaceSettings newValue) {
    if (oldValue == null) {
      // This is when settings are loaded, not really a user change
      return;
    }
    if (!Objects.equals(oldValue.getExcludedRules(), newValue.getExcludedRules()) ||
      !Objects.equals(oldValue.getIncludedRules(), newValue.getIncludedRules()) ||
      !Objects.equals(oldValue.getRuleParameters(), newValue.getRuleParameters())) {
      analyzeAllUnboundOpenFiles();
      analyzeAllOpenNotebooks();
    }
  }

  @Override
  public void onChange(@Nullable WorkspaceFolderWrapper folder, @Nullable WorkspaceFolderSettings oldValue, WorkspaceFolderSettings newValue) {
    if (oldValue == null) {
      // This is when settings are loaded, not really a user change
      return;
    }
    if (!Objects.equals(oldValue.getPathToCompileCommands(), newValue.getPathToCompileCommands())) {
      analyzeAllOpenCOrCppFilesInFolder(folder);
    }
  }

  public void analyzeAllOpenCOrCppFilesInFolder(@Nullable WorkspaceFolderWrapper folder) {
    var openedCorCppFileUrisInFolder = openFilesCache.getAll().stream()
      .filter(VersionedOpenFile::isCOrCpp)
      .filter(f -> belongToFolder(folder, f.getUri()))
      .toList();
    analyseNotIgnoredFiles(openedCorCppFileUrisInFolder);
  }

  public void scanForHotspotsInFiles(List files) {
    analyzeAsync(AnalysisParams.newAnalysisParams(files).withOnlyHotspots().withProgress());
  }

  private void analyzeAllUnboundOpenFiles() {
    var openedUnboundFileUris = openFilesCache.getAll().stream()
      .filter(f -> bindingManager.getBinding(f.getUri()).isEmpty())
      .toList();
    analyseNotIgnoredFiles(openedUnboundFileUris);
  }

  private void analyzeAllOpenNotebooks() {
    var openNotebookUris = openNotebooksCache.getAll().stream()
      .map(VersionedOpenNotebook::asVersionedOpenFile)
      .toList();
    analyseNotIgnoredFiles(openNotebookUris);
  }

  private void analyzeAllOpenJavaFiles() {
    var openedJavaFileUris = openFilesCache.getAll().stream()
      .filter(VersionedOpenFile::isJava)
      .toList();
    analyseNotIgnoredFiles(openedJavaFileUris);
  }

  public void didClasspathUpdate() {
    analyzeAllOpenJavaFiles();
  }

  public void didServerModeChange(SonarLintExtendedLanguageServer.ServerMode serverMode) {
    if (serverMode == SonarLintExtendedLanguageServer.ServerMode.STANDARD) {
      analyzeAllOpenJavaFiles();
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy