org.sonarsource.sonarlint.ls.SonarLintLanguageServer Maven / Gradle / Ivy
/*
* 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 com.google.gson.JsonPrimitive;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.CodeActionParams;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.DidChangeConfigurationParams;
import org.eclipse.lsp4j.DidChangeNotebookDocumentParams;
import org.eclipse.lsp4j.DidChangeTextDocumentParams;
import org.eclipse.lsp4j.DidChangeWatchedFilesParams;
import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams;
import org.eclipse.lsp4j.DidCloseNotebookDocumentParams;
import org.eclipse.lsp4j.DidCloseTextDocumentParams;
import org.eclipse.lsp4j.DidOpenNotebookDocumentParams;
import org.eclipse.lsp4j.DidOpenTextDocumentParams;
import org.eclipse.lsp4j.DidSaveNotebookDocumentParams;
import org.eclipse.lsp4j.DidSaveTextDocumentParams;
import org.eclipse.lsp4j.ExecuteCommandOptions;
import org.eclipse.lsp4j.ExecuteCommandParams;
import org.eclipse.lsp4j.InitializeParams;
import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.MessageParams;
import org.eclipse.lsp4j.MessageType;
import org.eclipse.lsp4j.NotebookDocumentSyncRegistrationOptions;
import org.eclipse.lsp4j.NotebookSelector;
import org.eclipse.lsp4j.NotebookSelectorCell;
import org.eclipse.lsp4j.ServerCapabilities;
import org.eclipse.lsp4j.ServerInfo;
import org.eclipse.lsp4j.SetTraceParams;
import org.eclipse.lsp4j.TextDocumentSyncKind;
import org.eclipse.lsp4j.TextDocumentSyncOptions;
import org.eclipse.lsp4j.WindowClientCapabilities;
import org.eclipse.lsp4j.WorkDoneProgressCancelParams;
import org.eclipse.lsp4j.WorkspaceFoldersOptions;
import org.eclipse.lsp4j.WorkspaceServerCapabilities;
import org.eclipse.lsp4j.jsonrpc.CompletableFutures;
import org.eclipse.lsp4j.jsonrpc.Launcher;
import org.eclipse.lsp4j.jsonrpc.ResponseErrorException;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.jsonrpc.messages.ResponseError;
import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode;
import org.eclipse.lsp4j.services.NotebookDocumentService;
import org.eclipse.lsp4j.services.TextDocumentService;
import org.eclipse.lsp4j.services.WorkspaceService;
import org.sonarsource.sonarlint.core.SonarLintBackendImpl;
import org.sonarsource.sonarlint.core.clientapi.backend.analysis.GetSupportedFilePatternsParams;
import org.sonarsource.sonarlint.core.clientapi.backend.analysis.GetSupportedFilePatternsResponse;
import org.sonarsource.sonarlint.core.clientapi.backend.binding.GetBindingSuggestionParams;
import org.sonarsource.sonarlint.core.clientapi.backend.connection.auth.HelpGenerateUserTokenResponse;
import org.sonarsource.sonarlint.core.clientapi.backend.connection.validate.ValidateConnectionParams;
import org.sonarsource.sonarlint.core.clientapi.backend.hotspot.CheckStatusChangePermittedParams;
import org.sonarsource.sonarlint.core.clientapi.backend.hotspot.HotspotStatus;
import org.sonarsource.sonarlint.core.clientapi.backend.hotspot.OpenHotspotInBrowserParams;
import org.sonarsource.sonarlint.core.clientapi.backend.issue.AddIssueCommentParams;
import org.sonarsource.sonarlint.core.clientapi.backend.issue.ReopenIssueResponse;
import org.sonarsource.sonarlint.core.clientapi.client.binding.GetBindingSuggestionsResponse;
import org.sonarsource.sonarlint.core.commons.Language;
import org.sonarsource.sonarlint.core.commons.SonarLintUserHome;
import org.sonarsource.sonarlint.ls.SonarLintExtendedLanguageClient.ConnectionCheckResult;
import org.sonarsource.sonarlint.ls.backend.BackendInitParams;
import org.sonarsource.sonarlint.ls.backend.BackendServiceFacade;
import org.sonarsource.sonarlint.ls.clientapi.SonarLintVSCodeClient;
import org.sonarsource.sonarlint.ls.connected.DelegatingIssue;
import org.sonarsource.sonarlint.ls.connected.ProjectBindingManager;
import org.sonarsource.sonarlint.ls.connected.TaintIssuesUpdater;
import org.sonarsource.sonarlint.ls.connected.TaintVulnerabilitiesCache;
import org.sonarsource.sonarlint.ls.connected.api.RequestsHandlerServer;
import org.sonarsource.sonarlint.ls.connected.events.ServerSentEventsHandler;
import org.sonarsource.sonarlint.ls.connected.events.ServerSentEventsHandlerService;
import org.sonarsource.sonarlint.ls.connected.notifications.SmartNotifications;
import org.sonarsource.sonarlint.ls.connected.notifications.TaintVulnerabilityRaisedNotification;
import org.sonarsource.sonarlint.ls.connected.sync.ServerSynchronizer;
import org.sonarsource.sonarlint.ls.file.FileTypeClassifier;
import org.sonarsource.sonarlint.ls.file.OpenFilesCache;
import org.sonarsource.sonarlint.ls.file.VersionedOpenFile;
import org.sonarsource.sonarlint.ls.folders.ModuleEventsProcessor;
import org.sonarsource.sonarlint.ls.folders.WorkspaceFolderBranchManager;
import org.sonarsource.sonarlint.ls.folders.WorkspaceFoldersManager;
import org.sonarsource.sonarlint.ls.folders.WorkspaceFoldersProvider;
import org.sonarsource.sonarlint.ls.java.JavaConfigCache;
import org.sonarsource.sonarlint.ls.log.LanguageClientLogOutput;
import org.sonarsource.sonarlint.ls.log.LanguageClientLogger;
import org.sonarsource.sonarlint.ls.notebooks.NotebookDiagnosticPublisher;
import org.sonarsource.sonarlint.ls.notebooks.OpenNotebooksCache;
import org.sonarsource.sonarlint.ls.notebooks.VersionedOpenNotebook;
import org.sonarsource.sonarlint.ls.progress.ProgressManager;
import org.sonarsource.sonarlint.ls.settings.ServerConnectionSettings;
import org.sonarsource.sonarlint.ls.settings.SettingsManager;
import org.sonarsource.sonarlint.ls.settings.WorkspaceFolderSettingsChangeListener;
import org.sonarsource.sonarlint.ls.settings.WorkspaceSettingsChangeListener;
import org.sonarsource.sonarlint.ls.standalone.StandaloneEngineManager;
import org.sonarsource.sonarlint.ls.telemetry.SonarLintTelemetry;
import org.sonarsource.sonarlint.ls.telemetry.TelemetryInitParams;
import org.sonarsource.sonarlint.ls.util.EnumLabelsMapper;
import org.sonarsource.sonarlint.ls.util.ExitingInputStream;
import org.sonarsource.sonarlint.ls.util.Utils;
import static java.lang.String.format;
import static java.net.URI.create;
import static java.util.Optional.ofNullable;
import static org.sonarsource.sonarlint.ls.CommandManager.SONARLINT_OPEN_RULE_DESCRIPTION_FROM_CODE_ACTION_COMMAND;
import static org.sonarsource.sonarlint.ls.CommandManager.SONARLINT_SHOW_SECURITY_HOTSPOT_FLOWS;
import static org.sonarsource.sonarlint.ls.SonarLintExtendedLanguageClient.ConnectionCheckResult.failure;
import static org.sonarsource.sonarlint.ls.SonarLintExtendedLanguageClient.ConnectionCheckResult.success;
import static org.sonarsource.sonarlint.ls.util.Utils.getConnectionNameFromConnectionCheckParams;
import static org.sonarsource.sonarlint.ls.util.Utils.getValidateConnectionParamsForNewConnection;
import static org.sonarsource.sonarlint.ls.util.Utils.hotspotStatusOfTitle;
import static org.sonarsource.sonarlint.ls.util.Utils.hotspotStatusValueOfHotspotReviewStatus;
public class SonarLintLanguageServer implements SonarLintExtendedLanguageServer, WorkspaceService, TextDocumentService, NotebookDocumentService {
public static final String JUPYTER_NOTEBOOK_TYPE = "jupyter-notebook";
public static final String PYTHON_LANGUAGE = "python";
private final SonarLintExtendedLanguageClient client;
private final SonarLintTelemetry telemetry;
private final WorkspaceFoldersManager workspaceFoldersManager;
private final SettingsManager settingsManager;
private final ProjectBindingManager bindingManager;
private final AnalysisScheduler analysisScheduler;
private final TaintVulnerabilitiesCache taintVulnerabilitiesCache;
private final OpenFilesCache openFilesCache;
private final OpenNotebooksCache openNotebooksCache;
private final EnginesFactory enginesFactory;
private final StandaloneEngineManager standaloneEngineManager;
private final CommandManager commandManager;
private final ProgressManager progressManager;
private final ExecutorService threadPool;
private final RequestsHandlerServer requestsHandlerServer;
private final WorkspaceFolderBranchManager branchManager;
private final JavaConfigCache javaConfigCache;
private final IssuesCache issuesCache;
private final IssuesCache securityHotspotsCache;
private final DiagnosticPublisher diagnosticPublisher;
private final ScmIgnoredCache scmIgnoredCache;
private final ServerSynchronizer serverSynchronizer;
private final LanguageClientLogger lsLogOutput;
private final TaintIssuesUpdater taintIssuesUpdater;
private final NotebookDiagnosticPublisher notebookDiagnosticPublisher;
private String appName;
/**
* Keep track of value 'sonarlint.trace.server' on client side. Not used currently, but keeping it just in case.
*/
private TraceValue traceLevel;
private final ModuleEventsProcessor moduleEventsProcessor;
private final BackendServiceFacade backendServiceFacade;
private final Collection analyzers;
private final CountDownLatch shutdownLatch;
SonarLintLanguageServer(InputStream inputStream, OutputStream outputStream, Collection analyzers) {
this.threadPool = Executors.newCachedThreadPool(Utils.threadFactory("SonarLint LSP message processor", false));
var input = new ExitingInputStream(inputStream, this);
var launcher = new Launcher.Builder()
.setLocalService(this)
.setRemoteInterface(SonarLintExtendedLanguageClient.class)
.setInput(input)
.setOutput(outputStream)
.setExecutorService(threadPool)
.create();
this.analyzers = analyzers;
this.client = launcher.getRemoteProxy();
this.lsLogOutput = new LanguageClientLogger(this.client);
var globalLogOutput = new LanguageClientLogOutput(lsLogOutput, false);
this.openFilesCache = new OpenFilesCache(lsLogOutput);
this.issuesCache = new IssuesCache();
this.securityHotspotsCache = new IssuesCache();
this.taintVulnerabilitiesCache = new TaintVulnerabilitiesCache();
this.notebookDiagnosticPublisher = new NotebookDiagnosticPublisher(client, issuesCache);
this.openNotebooksCache = new OpenNotebooksCache(lsLogOutput, notebookDiagnosticPublisher);
this.notebookDiagnosticPublisher.setOpenNotebooksCache(openNotebooksCache);
this.diagnosticPublisher = new DiagnosticPublisher(client, taintVulnerabilitiesCache, issuesCache, securityHotspotsCache, openNotebooksCache);
this.progressManager = new ProgressManager(client, globalLogOutput);
this.requestsHandlerServer = new RequestsHandlerServer(client);
var vsCodeClient = new SonarLintVSCodeClient(client, requestsHandlerServer, globalLogOutput);
this.backendServiceFacade = new BackendServiceFacade(new SonarLintBackendImpl(vsCodeClient));
vsCodeClient.setBackendServiceFacade(backendServiceFacade);
this.workspaceFoldersManager = new WorkspaceFoldersManager(backendServiceFacade, globalLogOutput);
this.settingsManager = new SettingsManager(this.client, this.workspaceFoldersManager, backendServiceFacade, globalLogOutput);
vsCodeClient.setSettingsManager(settingsManager);
backendServiceFacade.setSettingsManager(settingsManager);
var nodeJsRuntime = new NodeJsRuntime(settingsManager);
var fileTypeClassifier = new FileTypeClassifier(globalLogOutput);
javaConfigCache = new JavaConfigCache(client, openFilesCache, globalLogOutput);
this.enginesFactory = new EnginesFactory(analyzers, getEmbeddedPluginsToPath(), globalLogOutput, nodeJsRuntime,
new WorkspaceFoldersProvider(workspaceFoldersManager, fileTypeClassifier, javaConfigCache));
this.standaloneEngineManager = new StandaloneEngineManager(enginesFactory);
this.settingsManager.addListener(lsLogOutput);
this.bindingManager = new ProjectBindingManager(enginesFactory, workspaceFoldersManager, settingsManager, client, globalLogOutput,
taintVulnerabilitiesCache, diagnosticPublisher, backendServiceFacade, openNotebooksCache);
vsCodeClient.setBindingManager(bindingManager);
this.telemetry = new SonarLintTelemetry(settingsManager, bindingManager, nodeJsRuntime, backendServiceFacade, globalLogOutput);
backendServiceFacade.setTelemetry(telemetry);
this.settingsManager.addListener(telemetry);
this.settingsManager.addListener((WorkspaceSettingsChangeListener) bindingManager);
this.settingsManager.addListener((WorkspaceFolderSettingsChangeListener) bindingManager);
this.workspaceFoldersManager.addListener(settingsManager);
var smartNotifications = new SmartNotifications(client, telemetry);
vsCodeClient.setSmartNotifications(smartNotifications);
var skippedPluginsNotifier = new SkippedPluginsNotifier(client);
this.scmIgnoredCache = new ScmIgnoredCache(client, globalLogOutput);
this.moduleEventsProcessor = new ModuleEventsProcessor(standaloneEngineManager, workspaceFoldersManager, bindingManager, fileTypeClassifier, javaConfigCache);
var analysisTaskExecutor = new AnalysisTaskExecutor(scmIgnoredCache, lsLogOutput, globalLogOutput, workspaceFoldersManager, bindingManager, javaConfigCache, settingsManager,
fileTypeClassifier, issuesCache, securityHotspotsCache, taintVulnerabilitiesCache, telemetry, skippedPluginsNotifier, standaloneEngineManager, diagnosticPublisher,
client, openNotebooksCache, notebookDiagnosticPublisher, progressManager);
this.analysisScheduler = new AnalysisScheduler(lsLogOutput, workspaceFoldersManager, bindingManager, openFilesCache, openNotebooksCache, analysisTaskExecutor, client);
this.workspaceFoldersManager.addListener(moduleEventsProcessor);
bindingManager.setAnalysisManager(analysisScheduler);
this.settingsManager.addListener((WorkspaceSettingsChangeListener) analysisScheduler);
this.settingsManager.addListener((WorkspaceFolderSettingsChangeListener) analysisScheduler);
this.serverSynchronizer = new ServerSynchronizer(client, progressManager, bindingManager, analysisScheduler, backendServiceFacade, globalLogOutput);
this.commandManager = new CommandManager(client, settingsManager, bindingManager, serverSynchronizer, telemetry, taintVulnerabilitiesCache,
issuesCache, securityHotspotsCache, backendServiceFacade, workspaceFoldersManager, openNotebooksCache, globalLogOutput);
var taintVulnerabilityRaisedNotification = new TaintVulnerabilityRaisedNotification(client, commandManager);
ServerSentEventsHandlerService serverSentEventsHandler = new ServerSentEventsHandler(bindingManager, taintVulnerabilitiesCache,
taintVulnerabilityRaisedNotification, settingsManager, workspaceFoldersManager, analysisScheduler);
vsCodeClient.setServerSentEventsHandlerService(serverSentEventsHandler);
this.branchManager = new WorkspaceFolderBranchManager(client, bindingManager, backendServiceFacade, globalLogOutput);
this.bindingManager.setBranchResolver(branchManager::getReferenceBranchNameForFolder);
this.workspaceFoldersManager.addListener(this.branchManager);
this.workspaceFoldersManager.setBindingManager(bindingManager);
this.taintIssuesUpdater = new TaintIssuesUpdater(bindingManager, taintVulnerabilitiesCache, workspaceFoldersManager, settingsManager,
diagnosticPublisher, backendServiceFacade, globalLogOutput);
var cleanAsYouCodeManager = new CleanAsYouCodeManager(diagnosticPublisher, openFilesCache, backendServiceFacade);
this.settingsManager.addListener(cleanAsYouCodeManager);
this.shutdownLatch = new CountDownLatch(1);
launcher.startListening();
}
static SonarLintLanguageServer bySocket(int port, Collection analyzers) throws IOException {
var socket = new Socket("localhost", port);
return new SonarLintLanguageServer(socket.getInputStream(), socket.getOutputStream(), analyzers);
}
static SonarLintLanguageServer byStdio(List analyzers) {
return new SonarLintLanguageServer(System.in, System.out, analyzers);
}
@Override
public CompletableFuture initialize(InitializeParams params) {
return CompletableFutures.computeAsync(cancelToken -> {
cancelToken.checkCanceled();
this.traceLevel = parseTraceLevel(params.getTrace());
boolean workDoneSupportedByClient = ofNullable(params.getCapabilities())
.flatMap(capabilities -> ofNullable(capabilities.getWindow()))
.map(WindowClientCapabilities::getWorkDoneProgress)
.orElse(false);
progressManager.setWorkDoneProgressSupportedByClient(workDoneSupportedByClient);
workspaceFoldersManager.initialize(params.getWorkspaceFolders());
var options = Utils.parseToMap(params.getInitializationOptions());
if (options == null) {
options = Collections.emptyMap();
}
var productKey = (String) options.get("productKey");
// deprecated, will be ignored when productKey present
var telemetryStorage = (String) options.get("telemetryStorage");
var productName = (String) options.get("productName");
var productVersion = (String) options.get("productVersion");
var clientInfo = ofNullable(params.getClientInfo());
this.appName = clientInfo.map(ci -> ci.getName()).orElse("Unknown");
var workspaceName = (String) options.get("workspaceName");
var clientVersion = clientInfo.map(ci -> ci.getVersion()).orElse("Unknown");
var ideVersion = appName + " " + clientVersion;
var firstSecretDetected = (boolean) options.getOrDefault("firstSecretDetected", false);
var platform = (String) options.get("platform");
var architecture = (String) options.get("architecture");
var additionalAttributes = (Map) options.getOrDefault("additionalAttributes", Map.of());
var showVerboseLogs = (boolean) options.getOrDefault("showVerboseLogs", true);
var userAgent = productName + " " + productVersion;
lsLogOutput.initialize(showVerboseLogs);
analysisScheduler.initialize();
diagnosticPublisher.initialize(firstSecretDetected);
requestsHandlerServer.initialize(clientVersion, workspaceName);
backendServiceFacade.setTelemetryInitParams(new TelemetryInitParams(productKey, telemetryStorage,
productName, productVersion, ideVersion, platform, architecture, additionalAttributes));
enginesFactory.setOmnisharpDirectory((String) additionalAttributes.get("omnisharpDirectory"));
var c = new ServerCapabilities();
c.setTextDocumentSync(getTextDocumentSyncOptions());
c.setCodeActionProvider(true);
var executeCommandOptions = new ExecuteCommandOptions(CommandManager.SONARLINT_SERVERSIDE_COMMANDS);
executeCommandOptions.setWorkDoneProgress(true);
c.setExecuteCommandProvider(executeCommandOptions);
c.setWorkspace(getWorkspaceServerCapabilities());
if (isEnableNotebooks(options)) {
setNotebookSyncOptions(c);
}
var info = new ServerInfo("SonarLint Language Server", getServerVersion("slls-version.txt"));
provideBackendInitData(productKey, userAgent);
return new InitializeResult(c, info);
});
}
@VisibleForTesting
public static boolean isEnableNotebooks(Map options) {
return (boolean) options.getOrDefault("enableNotebooks", false);
}
private static void setNotebookSyncOptions(ServerCapabilities c) {
var noteBookDocumentSyncOptions = new NotebookDocumentSyncRegistrationOptions();
var notebookSelector = new NotebookSelector();
notebookSelector.setNotebook(JUPYTER_NOTEBOOK_TYPE);
notebookSelector.setCells(List.of(new NotebookSelectorCell(PYTHON_LANGUAGE)));
noteBookDocumentSyncOptions.setNotebookSelector(List.of(notebookSelector));
c.setNotebookDocumentSync(noteBookDocumentSyncOptions);
}
@CheckForNull
static String getServerVersion(String fileName) {
var classLoader = ClassLoader.getSystemClassLoader();
try (var is = classLoader.getResourceAsStream(fileName)) {
try (var isr = new InputStreamReader(is, StandardCharsets.UTF_8);
var reader = new BufferedReader(isr)) {
return reader.lines().findFirst().orElse(null);
}
} catch (IOException e) {
throw new IllegalStateException("Unable to read server version", e);
}
}
private static WorkspaceServerCapabilities getWorkspaceServerCapabilities() {
var options = new WorkspaceFoldersOptions();
options.setSupported(true);
options.setChangeNotifications(true);
var capabilities = new WorkspaceServerCapabilities();
capabilities.setWorkspaceFolders(options);
return capabilities;
}
private static TextDocumentSyncOptions getTextDocumentSyncOptions() {
var textDocumentSyncOptions = new TextDocumentSyncOptions();
textDocumentSyncOptions.setOpenClose(true);
textDocumentSyncOptions.setChange(TextDocumentSyncKind.Full);
return textDocumentSyncOptions;
}
@Override
public CompletableFuture
© 2015 - 2025 Weber Informatics LLC | Privacy Policy