diff --git a/its/plugin/pom.xml b/its/plugin/pom.xml
index 2e9e18113da..3266219a6c1 100644
--- a/its/plugin/pom.xml
+++ b/its/plugin/pom.xml
@@ -22,6 +22,7 @@
plugins
tests
sonarlint-tests
+ sonarlint-rpc-tests
diff --git a/its/plugin/sonarlint-rpc-tests/pom.xml b/its/plugin/sonarlint-rpc-tests/pom.xml
new file mode 100644
index 00000000000..d0bbb21fc79
--- /dev/null
+++ b/its/plugin/sonarlint-rpc-tests/pom.xml
@@ -0,0 +1,169 @@
+
+
+ 4.0.0
+
+
+ org.sonarsource.javascript
+ javascript-it-plugin
+ 10.17.0-SNAPSHOT
+
+
+ javascript-it-plugin-sonarlint-rpc-tests
+ JavaScript :: IT :: Plugin :: SonarLint RPC Tests
+
+
+ -server
+ 10.8.0.79183
+
+
+
+
+
+ com.google.code.gson
+ gson
+ test
+
+
+ org.sonarsource.analyzer-commons
+ sonar-analyzer-commons
+ test
+
+
+ org.sonarsource.sonarlint.core
+ sonarlint-core
+ ${sonarlint.plugin.api.version}
+
+
+ org.sonarsource.sonarlint.core
+ sonarlint-rpc-protocol
+ ${sonarlint.plugin.api.version}
+ test
+
+
+ org.sonarsource.sonarlint.core
+ sonarlint-rpc-impl
+ ${sonarlint.plugin.api.version}
+ test
+
+
+ org.sonarsource.sonarlint.core
+ sonarlint-rpc-java-client
+ ${sonarlint.plugin.api.version}
+ test
+
+
+ org.sonarsource.sonarlint.ls
+ sonarlint-language-server
+ 3.13.0.75653
+ test
+
+
+ org.awaitility
+ awaitility
+ 4.2.0
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+ org.assertj
+ assertj-core
+
+
+ com.google.code.findbugs
+ jsr305
+ provided
+
+
+ commons-io
+ commons-io
+ test
+
+
+ org.awaitility
+ awaitility
+
+
+
+
+
+
+ maven-surefire-plugin
+
+
+ **/*Test.java
+
+
+
+
+
+
+
+
+
+ qa
+
+
+ env.SONARSOURCE_QA
+ true
+
+
+
+
+
+ maven-dependency-plugin
+
+
+ copy-plugin
+ generate-test-resources
+
+ copy
+
+
+
+
+ ${project.groupId}
+ sonar-javascript-plugin
+ sonar-plugin
+ true
+
+
+ ${project.groupId}
+ sonar-javascript-plugin
+ win-x64
+ sonar-plugin
+ true
+
+
+ ${project.groupId}
+ sonar-javascript-plugin
+ linux-x64
+ sonar-plugin
+ true
+
+
+ ${project.groupId}
+ sonar-javascript-plugin
+ multi
+ sonar-plugin
+ true
+
+
+ ../../../sonar-plugin/sonar-javascript-plugin/target
+ true
+ true
+
+
+
+
+
+
+
+
+
+
diff --git a/its/plugin/sonarlint-rpc-tests/src/test/java/com/sonar/javascript/it/plugin/sonarlint/tests/AbstractLanguageServerMediumTests.java b/its/plugin/sonarlint-rpc-tests/src/test/java/com/sonar/javascript/it/plugin/sonarlint/tests/AbstractLanguageServerMediumTests.java
new file mode 100644
index 00000000000..520cd009bfe
--- /dev/null
+++ b/its/plugin/sonarlint-rpc-tests/src/test/java/com/sonar/javascript/it/plugin/sonarlint/tests/AbstractLanguageServerMediumTests.java
@@ -0,0 +1,818 @@
+/*
+ * 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 com.sonar.javascript.it.plugin.sonarlint.tests;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.sonarsource.sonarlint.ls.SonarLintLanguageServer.JUPYTER_NOTEBOOK_TYPE;
+import static org.sonarsource.sonarlint.ls.settings.SettingsManager.DOTNET_DEFAULT_SOLUTION_PATH;
+import static org.sonarsource.sonarlint.ls.settings.SettingsManager.OMNISHARP_LOAD_PROJECT_ON_DEMAND;
+import static org.sonarsource.sonarlint.ls.settings.SettingsManager.OMNISHARP_PROJECT_LOAD_TIMEOUT;
+import static org.sonarsource.sonarlint.ls.settings.SettingsManager.OMNISHARP_USE_MODERN_NET;
+import static org.sonarsource.sonarlint.ls.settings.SettingsManager.SONARLINT_CONFIGURATION_NAMESPACE;
+import static org.sonarsource.sonarlint.ls.settings.SettingsManager.VSCODE_FILE_EXCLUDES;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+import javax.annotation.Nullable;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.ArrayUtils;
+import org.assertj.core.api.iterable.ThrowingExtractor;
+import org.eclipse.lsp4j.ClientCapabilities;
+import org.eclipse.lsp4j.ClientInfo;
+import org.eclipse.lsp4j.ConfigurationItem;
+import org.eclipse.lsp4j.ConfigurationParams;
+import org.eclipse.lsp4j.Diagnostic;
+import org.eclipse.lsp4j.DidChangeConfigurationParams;
+import org.eclipse.lsp4j.DidChangeNotebookDocumentParams;
+import org.eclipse.lsp4j.DidChangeTextDocumentParams;
+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.InitializeParams;
+import org.eclipse.lsp4j.InitializedParams;
+import org.eclipse.lsp4j.MessageActionItem;
+import org.eclipse.lsp4j.MessageParams;
+import org.eclipse.lsp4j.NotebookDocument;
+import org.eclipse.lsp4j.NotebookDocumentChangeEvent;
+import org.eclipse.lsp4j.NotebookDocumentClientCapabilities;
+import org.eclipse.lsp4j.NotebookDocumentIdentifier;
+import org.eclipse.lsp4j.NotebookDocumentSyncClientCapabilities;
+import org.eclipse.lsp4j.ProgressParams;
+import org.eclipse.lsp4j.PublishDiagnosticsParams;
+import org.eclipse.lsp4j.ShowMessageRequestParams;
+import org.eclipse.lsp4j.TextDocumentContentChangeEvent;
+import org.eclipse.lsp4j.TextDocumentIdentifier;
+import org.eclipse.lsp4j.TextDocumentItem;
+import org.eclipse.lsp4j.VersionedNotebookDocumentIdentifier;
+import org.eclipse.lsp4j.VersionedTextDocumentIdentifier;
+import org.eclipse.lsp4j.WindowClientCapabilities;
+import org.eclipse.lsp4j.WorkDoneProgressCreateParams;
+import org.eclipse.lsp4j.WorkspaceFolder;
+import org.eclipse.lsp4j.WorkspaceFoldersChangeEvent;
+import org.eclipse.lsp4j.jsonrpc.CompletableFutures;
+import org.eclipse.lsp4j.jsonrpc.Launcher;
+import org.eclipse.lsp4j.launch.LSPLauncher;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.sonarsource.sonarlint.core.rpc.protocol.client.binding.AssistBindingParams;
+import org.sonarsource.sonarlint.core.rpc.protocol.client.binding.SuggestBindingParams;
+import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.SuggestConnectionParams;
+import org.sonarsource.sonarlint.ls.ServerMain;
+import org.sonarsource.sonarlint.ls.SonarLintExtendedLanguageClient;
+import org.sonarsource.sonarlint.ls.SonarLintExtendedLanguageServer;
+import org.sonarsource.sonarlint.ls.SonarLintLanguageServer;
+import org.sonarsource.sonarlint.ls.commands.ShowAllLocationsCommand;
+import org.sonarsource.sonarlint.ls.settings.SettingsManager;
+import org.sonarsource.sonarlint.ls.telemetry.SonarLintTelemetry;
+import picocli.CommandLine;
+
+public abstract class AbstractLanguageServerMediumTests {
+ protected static final boolean COMMERCIAL_ENABLED = System.getProperty("commercial") != null;
+ private static final String CSHARP_OSS_PATH = fullPathToJar("sonarcsharp");
+ private static final String CSHARP_ENTERPRISE_PATH = COMMERCIAL_ENABLED ? fullPathToJar("csharpenterprise") : CSHARP_OSS_PATH;
+
+ private static final Set staticTempDirs = new HashSet<>();
+ private final Set instanceTempDirs = new HashSet<>();
+ Path temp;
+ protected Set toBeClosed = new HashSet<>();
+ protected Set notebooksToBeClosed = new HashSet<>();
+ protected Set foldersToRemove = new HashSet<>();
+ private static ServerSocket serverSocket;
+ protected static SonarLintExtendedLanguageServer lsProxy;
+ protected static FakeLanguageClient client;
+ private static List foundFileDtos = List.of();
+
+ @BeforeAll
+ static void startServer() throws Exception {
+ System.setProperty(SonarLintTelemetry.DISABLE_PROPERTY_KEY, "true");
+ SettingsManager.setSonarLintUserHomeOverride(makeStaticTempDir());
+ serverSocket = new ServerSocket(0);
+ var port = serverSocket.getLocalPort();
+
+ client = new FakeLanguageClient();
+
+ var executor = Executors.newFixedThreadPool(2);
+ var future = executor.submit(() -> {
+ Socket socket = serverSocket.accept();
+ Launcher clientSideLauncher = new LSPLauncher.Builder()
+ .setLocalService(client)
+ .setRemoteInterface(SonarLintExtendedLanguageServer.class)
+ .setInput(socket.getInputStream())
+ .setOutput(socket.getOutputStream())
+ .create();
+ clientSideLauncher.startListening();
+ return clientSideLauncher.getRemoteProxy();
+ });
+
+ var go = fullPathToJar("sonargo");
+ var iac = fullPathToJar("sonariac");
+ var html = fullPathToJar("sonarhtml");
+ var java = fullPathToJar("sonarjava");
+ var js = fullPathToJar("sonarjs");
+ var php = fullPathToJar("sonarphp");
+ var py = fullPathToJar("sonarpython");
+ var text = fullPathToJar("sonartext");
+ var xml = fullPathToJar("sonarxml");
+ var omnisharp = fullPathToJar("sonarlintomnisharp");
+ String[] languageServerArgs = new String[]{"-port", "" + port, "-analyzers", go, java, js, php, py, html, xml, text, iac, omnisharp};
+ if (COMMERCIAL_ENABLED) {
+ var cfamily = fullPathToJar("cfamily");
+ languageServerArgs = ArrayUtils.add(languageServerArgs, cfamily);
+ }
+
+ try {
+ var cmd = new CommandLine(new ServerMain());
+ var cmdOutput = new StringWriter();
+ cmd.setErr(new PrintWriter(cmdOutput));
+ cmd.setOut(new PrintWriter(cmdOutput));
+
+ var clonedArgs = ArrayUtils.clone(languageServerArgs);
+ executor.submit(() -> cmd.execute(clonedArgs));
+ executor.shutdown();
+ } catch (Exception e) {
+ e.printStackTrace();
+ future.get(1, TimeUnit.SECONDS);
+ if (!future.isDone()) {
+ future.cancel(true);
+ }
+ throw e;
+ }
+
+ lsProxy = future.get();
+ }
+ //https://github.com/SonarSource/sonarlint-language-server/blob/63e5ceef866c7a08e6f7c7d9d0f4020200dec720/src/test/java/org/sonarsource/sonarlint/ls/mediumtests/LanguageServerMediumTests.java
+ //https://github.com/SonarSource/sonarlint-core/blob/ddb7cfb8ecdbc703263c2662cc697292099759c8/medium-tests/src/test/java/mediumtest/analysis/AnalysisMediumTests.java#L980
+ protected static String fullPathToJar(String jarName) {
+ return Paths.get("target/plugins").resolve(jarName + ".jar").toAbsolutePath().toString();
+ }
+
+ protected static void initialize(Map initializeOptions, WorkspaceFolder... initFolders) throws InterruptedException, ExecutionException {
+ var initializeParams = getInitializeParams(initializeOptions, initFolders);
+ initializeParams.getCapabilities().setWindow(new WindowClientCapabilities());
+ var initializeResult = lsProxy.initialize(initializeParams).get();
+ assertThat(initializeResult.getServerInfo().getName()).isEqualTo("SonarLint Language Server");
+ assertThat(initializeResult.getServerInfo().getVersion()).isNotBlank();
+ if (SonarLintLanguageServer.isEnableNotebooks(initializeOptions)) {
+ assertThat(initializeResult.getCapabilities().getNotebookDocumentSync()).isNotNull();
+ } else {
+ assertThat(initializeResult.getCapabilities().getNotebookDocumentSync()).isNull();
+ }
+ lsProxy.initialized(new InitializedParams());
+ }
+
+ @NotNull
+ private static InitializeParams getInitializeParams(Map initializeOptions, WorkspaceFolder[] initFolders) {
+ var initializeParams = new InitializeParams();
+ initializeParams.setTrace("messages");
+
+ var actualInitOptions = new HashMap<>(initializeOptions);
+ if (initializeOptions.containsKey("additionalAttributes")) {
+ var additionalAttributes = new HashMap<>((Map)initializeOptions.get("additionalAttributes"));
+ additionalAttributes.put("csharpOssPath", CSHARP_OSS_PATH);
+ additionalAttributes.put("csharpEnterprisePath", CSHARP_ENTERPRISE_PATH);
+ actualInitOptions.put("additionalAttributes", additionalAttributes);
+ }
+ initializeParams.setInitializationOptions(actualInitOptions);
+
+ initializeParams.setWorkspaceFolders(List.of(initFolders));
+ initializeParams.setClientInfo(new ClientInfo("SonarLint LS Medium tests", "1.0"));
+ var clientCapabilities = new ClientCapabilities();
+ var notebookDocument = new NotebookDocumentClientCapabilities();
+ var synchronization = new NotebookDocumentSyncClientCapabilities();
+ synchronization.setDynamicRegistration(true);
+ synchronization.setExecutionSummarySupport(true);
+ notebookDocument.setSynchronization(synchronization);
+ clientCapabilities.setNotebookDocument(notebookDocument);
+ initializeParams.setCapabilities(clientCapabilities);
+ return initializeParams;
+ }
+
+ @AfterAll
+ public static void stopServer() throws Exception {
+ staticTempDirs.forEach(tempDirPath -> FileUtils.deleteQuietly(tempDirPath.toFile()));
+ staticTempDirs.clear();
+ System.clearProperty(SonarLintTelemetry.DISABLE_PROPERTY_KEY);
+ try {
+ if (lsProxy != null) {
+ // 20 seconds should be way enough time for the backend to stop
+ lsProxy.shutdown().get(20, SECONDS);
+ lsProxy.exit();
+ }
+ } finally {
+ serverSocket.close();
+ }
+ }
+
+ @BeforeEach
+ void cleanup() throws Exception {
+ temp = makeInstanceTempDir();
+ // Reset state on LS side
+ client.clear();
+ toBeClosed.clear();
+ notebooksToBeClosed.clear();
+
+ setupGlobalSettings(client.globalSettings);
+ setUpFolderSettings(client.folderSettings);
+
+ notifyConfigurationChangeOnClient();
+ verifyConfigurationChangeOnClient();
+ }
+
+ protected static void setUpFindFilesInFolderResponse(List foundFileDtos) {
+ AbstractLanguageServerMediumTests.foundFileDtos = foundFileDtos;
+ }
+
+ protected void setupGlobalSettings(Map globalSettings) {
+ // do nothing by default
+ }
+
+ protected void setUpFolderSettings(Map> folderSettings) {
+ // do nothing by default
+ }
+
+ protected void verifyConfigurationChangeOnClient() {
+ // do nothing by default
+ }
+
+ @AfterEach
+ final void closeFiles() {
+ // Close all opened files
+ for (var uri : toBeClosed) {
+ lsProxy.getTextDocumentService().didClose(new DidCloseTextDocumentParams(new TextDocumentIdentifier(uri)));
+ }
+ for (var uri : notebooksToBeClosed) {
+ lsProxy.getNotebookDocumentService().didClose(new DidCloseNotebookDocumentParams(new NotebookDocumentIdentifier(uri), List.of()));
+ }
+ foldersToRemove.forEach(folderUri -> lsProxy.getWorkspaceService().didChangeWorkspaceFolders(
+ new DidChangeWorkspaceFoldersParams(new WorkspaceFoldersChangeEvent(List.of(), List.of(new WorkspaceFolder(folderUri))))));
+ instanceTempDirs.forEach(tempDirPath -> FileUtils.deleteQuietly(tempDirPath.toFile()));
+ instanceTempDirs.clear();
+ }
+
+ protected static void assertLogContains(String msg) {
+ assertLogContainsPattern("\\[.*\\] " + Pattern.quote(msg) + ".*");
+ }
+
+ protected static void assertLogContainsPattern(String msgPattern) {
+ await().atMost(10, SECONDS).untilAsserted(() -> assertThat(client.logs).anyMatch(p -> p.getMessage().matches(msgPattern)));
+ }
+
+ protected String getUri(String filename) throws IOException {
+ var file = temp.resolve(filename);
+ Files.createFile(file);
+ return file.toUri().toString();
+ }
+
+ protected String getUri(String filename, Path tempDir) throws IOException {
+ var file = tempDir.resolve(filename);
+ Files.createFile(file);
+ return file.toUri().toString();
+ }
+
+ protected static void awaitLatch(CountDownLatch latch) {
+ try {
+ assertTrue(latch.await(15, TimeUnit.SECONDS));
+ } catch (InterruptedException e) {
+ fail(e);
+ }
+ }
+
+ protected static class FakeLanguageClient implements SonarLintExtendedLanguageClient {
+
+ Map> diagnostics = new ConcurrentHashMap<>();
+ Map> hotspots = new ConcurrentHashMap<>();
+ Queue logs = new ConcurrentLinkedQueue<>();
+ Map globalSettings = new HashMap<>();
+ Map> folderSettings = new HashMap<>();
+ Map javaConfigs = new HashMap<>();
+ Map referenceBranchNameByFolder = new HashMap<>();
+ Map scopeReadyForAnalysis = new HashMap<>();
+ CountDownLatch settingsLatch = new CountDownLatch(0);
+ CountDownLatch showRuleDescriptionLatch = new CountDownLatch(0);
+ CountDownLatch suggestBindingLatch = new CountDownLatch(0);
+ CountDownLatch readyForTestsLatch = new CountDownLatch(0);
+ ShowAllLocationsCommand.Param showIssueParams;
+ ShowFixSuggestionParams showFixSuggestionParams;
+ SuggestBindingParams suggestedBindings;
+ ShowRuleDescriptionParams ruleDesc;
+ boolean isIgnoredByScm = false;
+ boolean shouldAnalyseFile = true;
+ final AtomicInteger needCompilationDatabaseCalls = new AtomicInteger();
+ final Set openedLinks = new HashSet<>();
+ final Set shownMessages = new HashSet<>();
+ final Map newCodeDefinitionCache = new HashMap<>();
+
+ void clearHotspotsAndIssuesAndConfigScopeReadiness() {
+ scopeReadyForAnalysis.clear();
+ diagnostics.clear();
+ hotspots.clear();
+ }
+
+ void clear() {
+ diagnostics.clear();
+ hotspots.clear();
+ logs.clear();
+ shownMessages.clear();
+ globalSettings = new HashMap<>();
+ setDisableTelemetry(globalSettings, true);
+ folderSettings.clear();
+ settingsLatch = new CountDownLatch(0);
+ showRuleDescriptionLatch = new CountDownLatch(0);
+ suggestBindingLatch = new CountDownLatch(0);
+ readyForTestsLatch = new CountDownLatch(0);
+ needCompilationDatabaseCalls.set(0);
+ shouldAnalyseFile = true;
+ scopeReadyForAnalysis.clear();
+ suggestedBindings = null;
+ }
+
+ @Override
+ public void telemetryEvent(Object object) {
+ }
+
+ List getDiagnostics(String uri) {
+ return diagnostics.getOrDefault(uri, List.of());
+ }
+
+ List getHotspots(String uri) {
+ return hotspots.getOrDefault(uri, List.of());
+ }
+
+ @Override
+ public void publishDiagnostics(PublishDiagnosticsParams diagnostics) {
+ this.diagnostics.put(diagnostics.getUri(), diagnostics.getDiagnostics());
+ }
+
+ @Override
+ public void publishSecurityHotspots(PublishDiagnosticsParams diagnostics) {
+ this.hotspots.put(diagnostics.getUri(), diagnostics.getDiagnostics());
+ }
+
+ @Override
+ public void showMessage(MessageParams messageParams) {
+ shownMessages.add(messageParams);
+ }
+
+ @Override
+ public CompletableFuture showMessageRequest(ShowMessageRequestParams requestParams) {
+ return CompletableFuture.completedFuture(null);
+ }
+
+ @Override
+ public void logMessage(MessageParams message) {
+ // SSLRSQBR-72 This log is produced by analyzers ProgressReport, and keeps coming long after the analysis has completed. Just ignore
+ // it
+ if (!message.getMessage().contains("1/1 source files have been analyzed")) {
+ logs.add(message);
+ }
+ System.out.println(message.getMessage());
+ }
+
+ @Override
+ public void notifyProgress(ProgressParams params) {
+ System.out.println(params);
+ }
+
+ @Override
+ public CompletableFuture createProgress(WorkDoneProgressCreateParams params) {
+ return CompletableFuture.completedFuture(null);
+ }
+
+ @Override
+ public CompletableFuture> configuration(ConfigurationParams configurationParams) {
+ return CompletableFutures.computeAsync(cancelToken -> {
+ List